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
|
import qs.Common
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
|
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||||
@@ -23,8 +22,7 @@ Singleton {
|
|||||||
property bool qtThemingEnabled: false
|
property bool qtThemingEnabled: false
|
||||||
property bool systemThemeGenerationInProgress: false
|
property bool systemThemeGenerationInProgress: false
|
||||||
property string matugenJson: ""
|
property string matugenJson: ""
|
||||||
property var matugenColors: ({
|
property var matugenColors: ({})
|
||||||
})
|
|
||||||
property bool extractionRequested: false
|
property bool extractionRequested: false
|
||||||
property int colorUpdateTrigger: 0
|
property int colorUpdateTrigger: 0
|
||||||
property string lastWallpaperTimestamp: ""
|
property string lastWallpaperTimestamp: ""
|
||||||
@@ -48,13 +46,13 @@ Singleton {
|
|||||||
property color accentHi: primary
|
property color accentHi: primary
|
||||||
property color accentLo: secondary
|
property color accentLo: secondary
|
||||||
|
|
||||||
signal colorsUpdated()
|
signal colorsUpdated
|
||||||
|
|
||||||
function onLightModeChanged() {
|
function onLightModeChanged() {
|
||||||
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
||||||
colorUpdateTrigger++;
|
colorUpdateTrigger++;
|
||||||
colorsUpdated();
|
colorsUpdated();
|
||||||
|
|
||||||
if (typeof Theme !== "undefined" && Theme.isDynamicTheme) {
|
if (typeof Theme !== "undefined" && Theme.isDynamicTheme) {
|
||||||
generateSystemThemes();
|
generateSystemThemes();
|
||||||
}
|
}
|
||||||
@@ -98,12 +96,12 @@ Singleton {
|
|||||||
id: matugenCheck
|
id: matugenCheck
|
||||||
|
|
||||||
command: ["which", "matugen"]
|
command: ["which", "matugen"]
|
||||||
onExited: (code) => {
|
onExited: code => {
|
||||||
matugenAvailable = (code === 0);
|
matugenAvailable = (code === 0);
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
ToastService.wallpaperErrorStatus = "matugen_missing";
|
ToastService.wallpaperErrorStatus = "matugen_missing";
|
||||||
ToastService.showWarning("matugen not found - dynamic theming disabled");
|
ToastService.showWarning("matugen not found - dynamic theming disabled");
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
if (extractionRequested) {
|
if (extractionRequested) {
|
||||||
fileChecker.running = true;
|
fileChecker.running = true;
|
||||||
@@ -115,7 +113,7 @@ Singleton {
|
|||||||
id: fileChecker
|
id: fileChecker
|
||||||
|
|
||||||
command: ["test", "-r", wallpaperPath]
|
command: ["test", "-r", wallpaperPath]
|
||||||
onExited: (code) => {
|
onExited: code => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
matugenProcess.running = true;
|
matugenProcess.running = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -138,7 +136,7 @@ Singleton {
|
|||||||
if (!out.length) {
|
if (!out.length) {
|
||||||
ToastService.wallpaperErrorStatus = "error";
|
ToastService.wallpaperErrorStatus = "error";
|
||||||
ToastService.showError("Wallpaper Processing Failed");
|
ToastService.showError("Wallpaper Processing Failed");
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
root.matugenJson = out;
|
root.matugenJson = out;
|
||||||
@@ -156,7 +154,6 @@ Singleton {
|
|||||||
stderr: StdioCollector {
|
stderr: StdioCollector {
|
||||||
id: matugenErr
|
id: matugenErr
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateAppConfigs() {
|
function generateAppConfigs() {
|
||||||
@@ -166,18 +163,18 @@ Singleton {
|
|||||||
|
|
||||||
generateNiriConfig();
|
generateNiriConfig();
|
||||||
generateGhosttyConfig();
|
generateGhosttyConfig();
|
||||||
|
|
||||||
if (gtkThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) {
|
if (gtkThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) {
|
||||||
generateGtkThemes();
|
generateSystemThemes();
|
||||||
}
|
} else if (qtThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) {
|
||||||
if (qtThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) {
|
generateSystemThemes();
|
||||||
generateQtThemes();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateNiriConfig() {
|
function generateNiriConfig() {
|
||||||
var dark = matugenColors.colors.dark;
|
var dark = matugenColors.colors.dark;
|
||||||
if (!dark) return;
|
if (!dark)
|
||||||
|
return;
|
||||||
|
|
||||||
var bg = dark.background || "#1a1c1e";
|
var bg = dark.background || "#1a1c1e";
|
||||||
var primary = dark.primary || "#42a5f5";
|
var primary = dark.primary || "#42a5f5";
|
||||||
@@ -201,7 +198,8 @@ Singleton {
|
|||||||
function generateGhosttyConfig() {
|
function generateGhosttyConfig() {
|
||||||
var dark = matugenColors.colors.dark;
|
var dark = matugenColors.colors.dark;
|
||||||
var light = matugenColors.colors.light;
|
var light = matugenColors.colors.light;
|
||||||
if (!dark || !light) return;
|
if (!dark || !light)
|
||||||
|
return;
|
||||||
|
|
||||||
var bg = dark.background || "#1a1c1e";
|
var bg = dark.background || "#1a1c1e";
|
||||||
var fg = dark.on_background || "#e3e8ef";
|
var fg = dark.on_background || "#e3e8ef";
|
||||||
@@ -245,116 +243,108 @@ palette = 15=${fg_b}`;
|
|||||||
|
|
||||||
var ghosttyConfigDir = configDir + "/ghostty";
|
var ghosttyConfigDir = configDir + "/ghostty";
|
||||||
var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors";
|
var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors";
|
||||||
|
|
||||||
Quickshell.execDetached(["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`]);
|
Quickshell.execDetached(["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkGtkThemingAvailability() {
|
function checkGtkThemingAvailability() {
|
||||||
gtkAvailabilityChecker.running = true;
|
gtkAvailabilityChecker.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkQtThemingAvailability() {
|
function checkQtThemingAvailability() {
|
||||||
qtAvailabilityChecker.running = true;
|
qtAvailabilityChecker.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSystemThemes() {
|
function generateSystemThemes() {
|
||||||
if (systemThemeGenerationInProgress) {
|
if (systemThemeGenerationInProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wallpaperPath || wallpaperPath === "") {
|
if (!wallpaperPath || wallpaperPath === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
||||||
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
|
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
|
||||||
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
|
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
|
||||||
|
|
||||||
systemThemeGenerationInProgress = true;
|
systemThemeGenerationInProgress = true;
|
||||||
systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming];
|
systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming];
|
||||||
systemThemeGenerator.running = true;
|
systemThemeGenerator.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateGtkThemes() {
|
|
||||||
generateSystemThemes();
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateQtThemes() {
|
|
||||||
generateSystemThemes();
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreSystemThemes() {
|
function restoreSystemThemes() {
|
||||||
const shellDir = root.shellDir;
|
const shellDir = root.shellDir;
|
||||||
if (!shellDir) {
|
if (!shellDir) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
||||||
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
|
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
|
||||||
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
|
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
|
||||||
|
|
||||||
systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming];
|
systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming];
|
||||||
systemThemeRestoreProcess.running = true;
|
systemThemeRestoreProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: gtkAvailabilityChecker
|
id: gtkAvailabilityChecker
|
||||||
command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d " + configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"]
|
command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d " + configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"]
|
||||||
running: false
|
running: false
|
||||||
onExited: (exitCode) => {
|
onExited: exitCode => {
|
||||||
gtkThemingEnabled = (exitCode === 0);
|
gtkThemingEnabled = (exitCode === 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: qtAvailabilityChecker
|
id: qtAvailabilityChecker
|
||||||
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
|
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
|
||||||
running: false
|
running: false
|
||||||
onExited: (exitCode) => {
|
onExited: exitCode => {
|
||||||
qtThemingEnabled = (exitCode === 0);
|
qtThemingEnabled = (exitCode === 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: systemThemeGenerator
|
id: systemThemeGenerator
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
id: systemThemeStdout
|
id: systemThemeStdout
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr: StdioCollector {
|
stderr: StdioCollector {
|
||||||
id: systemThemeStderr
|
id: systemThemeStderr
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
onExited: exitCode => {
|
||||||
systemThemeGenerationInProgress = false;
|
systemThemeGenerationInProgress = false;
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text);
|
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: systemThemeRestoreProcess
|
id: systemThemeRestoreProcess
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
id: restoreThemeStdout
|
id: restoreThemeStdout
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr: StdioCollector {
|
stderr: StdioCollector {
|
||||||
id: restoreThemeStderr
|
id: restoreThemeStderr
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
onExited: exitCode => {
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
ToastService.showInfo("System themes restored to default");
|
ToastService.showInfo("System themes restored to default");
|
||||||
} else {
|
} else {
|
||||||
@@ -362,6 +352,4 @@ palette = 15=${fg_b}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ DankModal {
|
|||||||
border.width: 1
|
border.width: 1
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
ListView {
|
DankListView {
|
||||||
id: clipboardListView
|
id: clipboardListView
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
DankModal {
|
DankModal {
|
||||||
@@ -14,25 +15,20 @@ DankModal {
|
|||||||
property string powerConfirmMessage: ""
|
property string powerConfirmMessage: ""
|
||||||
|
|
||||||
function executePowerAction(action) {
|
function executePowerAction(action) {
|
||||||
|
|
||||||
let command = [];
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "logout":
|
case "logout":
|
||||||
command = ["niri", "msg", "action", "quit", "-s"];
|
NiriService.quit();
|
||||||
break;
|
break;
|
||||||
case "suspend":
|
case "suspend":
|
||||||
command = ["systemctl", "suspend"];
|
Quickshell.execDetached(["systemctl", "suspend"]);
|
||||||
break;
|
break;
|
||||||
case "reboot":
|
case "reboot":
|
||||||
command = ["systemctl", "reboot"];
|
Quickshell.execDetached(["systemctl", "reboot"]);
|
||||||
break;
|
break;
|
||||||
case "poweroff":
|
case "poweroff":
|
||||||
command = ["systemctl", "poweroff"];
|
Quickshell.execDetached(["systemctl", "poweroff"]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (command.length > 0) {
|
|
||||||
Quickshell.execDetached(command);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: powerConfirmVisible
|
visible: powerConfirmVisible
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import Quickshell.Widgets
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.AppDrawer
|
import qs.Modules.AppDrawer
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -284,17 +285,49 @@ DankModal {
|
|||||||
|
|
||||||
DankListView {
|
DankListView {
|
||||||
id: resultsList
|
id: resultsList
|
||||||
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
|
property int itemHeight: 60
|
||||||
|
property int iconSize: 40
|
||||||
|
property bool showDescription: true
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
|
||||||
|
signal keyboardNavigationReset()
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemHovered(int index)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var itemY = index * (itemHeight + itemSpacing);
|
||||||
|
var itemBottom = itemY + itemHeight;
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY;
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height;
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
visible: appLauncher.viewMode === "list"
|
visible: appLauncher.viewMode === "list"
|
||||||
model: appLauncher.model
|
model: appLauncher.model
|
||||||
currentIndex: appLauncher.selectedIndex
|
currentIndex: appLauncher.selectedIndex
|
||||||
itemHeight: 60
|
clip: true
|
||||||
iconSize: 40
|
spacing: itemSpacing
|
||||||
showDescription: true
|
focus: true
|
||||||
hoverUpdatesSelection: false
|
interactive: true
|
||||||
keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
onItemClicked: function(index, modelData) {
|
onItemClicked: function(index, modelData) {
|
||||||
appLauncher.launchApp(modelData);
|
appLauncher.launchApp(modelData);
|
||||||
}
|
}
|
||||||
@@ -307,24 +340,170 @@ DankModal {
|
|||||||
onKeyboardNavigationReset: {
|
onKeyboardNavigationReset: {
|
||||||
appLauncher.keyboardNavigationActive = false;
|
appLauncher.keyboardNavigationActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOn
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: resultsList.itemHeight
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: ListView.isCurrentItem ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: resultsList.iconSize
|
||||||
|
height: resultsList.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !iconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: resultsList.iconSize * 0.4
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - resultsList.iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: resultsList.showDescription && model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive)
|
||||||
|
resultsList.currentIndex = index;
|
||||||
|
|
||||||
|
resultsList.itemHovered(index);
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
resultsList.keyboardNavigationReset();
|
||||||
|
}
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsList.itemClicked(index, model);
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var globalPos = mapToGlobal(mouse.x, mouse.y);
|
||||||
|
resultsList.itemRightClicked(index, model, globalPos.x, globalPos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankGridView {
|
DankGridView {
|
||||||
id: resultsGrid
|
id: resultsGrid
|
||||||
|
|
||||||
|
property int currentIndex: appLauncher.selectedIndex
|
||||||
|
property int columns: 4
|
||||||
|
property bool adaptiveColumns: false
|
||||||
|
property int minCellWidth: 120
|
||||||
|
property int maxCellWidth: 160
|
||||||
|
property int cellPadding: 8
|
||||||
|
property real iconSizeRatio: 0.55
|
||||||
|
property int maxIconSize: 48
|
||||||
|
property int minIconSize: 32
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
|
|
||||||
|
signal keyboardNavigationReset()
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemHovered(int index)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var itemY = Math.floor(index / actualColumns) * cellHeight;
|
||||||
|
var itemBottom = itemY + cellHeight;
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY;
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height;
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
visible: appLauncher.viewMode === "grid"
|
visible: appLauncher.viewMode === "grid"
|
||||||
model: appLauncher.model
|
model: appLauncher.model
|
||||||
columns: 4
|
clip: true
|
||||||
adaptiveColumns: false
|
cellWidth: baseCellWidth
|
||||||
minCellWidth: 120
|
cellHeight: baseCellHeight
|
||||||
maxCellWidth: 160
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
iconSizeRatio: 0.55
|
rightMargin: leftMargin
|
||||||
maxIconSize: 48
|
focus: true
|
||||||
currentIndex: appLauncher.selectedIndex
|
interactive: true
|
||||||
hoverUpdatesSelection: false
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
onItemClicked: function(index, modelData) {
|
onItemClicked: function(index, modelData) {
|
||||||
appLauncher.launchApp(modelData);
|
appLauncher.launchApp(modelData);
|
||||||
}
|
}
|
||||||
@@ -337,6 +516,103 @@ DankModal {
|
|||||||
onKeyboardNavigationReset: {
|
onKeyboardNavigationReset: {
|
||||||
appLauncher.keyboardNavigationActive = false;
|
appLauncher.keyboardNavigationActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: resultsGrid.cellWidth - resultsGrid.cellPadding
|
||||||
|
height: resultsGrid.cellHeight - resultsGrid.cellPadding
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: resultsGrid.currentIndex === index ? 2 : 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
|
||||||
|
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !iconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: resultsGrid.cellWidth - 12
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive)
|
||||||
|
resultsGrid.currentIndex = index;
|
||||||
|
|
||||||
|
resultsGrid.itemHovered(index);
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
resultsGrid.keyboardNavigationReset();
|
||||||
|
}
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
resultsGrid.itemClicked(index, model);
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var globalPos = mapToGlobal(mouse.x, mouse.y);
|
||||||
|
resultsGrid.itemRightClicked(index, model, globalPos.x, globalPos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,17 +370,49 @@ PanelWindow {
|
|||||||
|
|
||||||
DankListView {
|
DankListView {
|
||||||
id: appList
|
id: appList
|
||||||
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
|
property int itemHeight: 72
|
||||||
|
property int iconSize: 56
|
||||||
|
property bool showDescription: true
|
||||||
|
property int itemSpacing: Theme.spacingS
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
|
||||||
|
signal keyboardNavigationReset()
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemHovered(int index)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var itemY = index * (itemHeight + itemSpacing);
|
||||||
|
var itemBottom = itemY + itemHeight;
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY;
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height;
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
visible: appLauncher.viewMode === "list"
|
visible: appLauncher.viewMode === "list"
|
||||||
model: appLauncher.model
|
model: appLauncher.model
|
||||||
currentIndex: appLauncher.selectedIndex
|
currentIndex: appLauncher.selectedIndex
|
||||||
itemHeight: 72
|
clip: true
|
||||||
iconSize: 56
|
spacing: itemSpacing
|
||||||
showDescription: true
|
focus: true
|
||||||
hoverUpdatesSelection: false
|
interactive: true
|
||||||
keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
onItemClicked: function(index, modelData) {
|
onItemClicked: function(index, modelData) {
|
||||||
appLauncher.launchApp(modelData);
|
appLauncher.launchApp(modelData);
|
||||||
}
|
}
|
||||||
@@ -393,20 +425,170 @@ PanelWindow {
|
|||||||
onKeyboardNavigationReset: {
|
onKeyboardNavigationReset: {
|
||||||
appLauncher.keyboardNavigationActive = false;
|
appLauncher.keyboardNavigationActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOn
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: appList.itemHeight
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
color: ListView.isCurrentItem ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: ListView.isCurrentItem ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: appList.iconSize
|
||||||
|
height: appList.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !iconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: appList.iconSize * 0.4
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - appList.iconSize - Theme.spacingL
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: model.comment || "Application"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: appList.showDescription && model.comment && model.comment.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive)
|
||||||
|
appList.currentIndex = index;
|
||||||
|
|
||||||
|
appList.itemHovered(index);
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
appList.keyboardNavigationReset();
|
||||||
|
}
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
appList.itemClicked(index, model);
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var globalPos = mapToGlobal(mouse.x, mouse.y);
|
||||||
|
appList.itemRightClicked(index, model, globalPos.x, globalPos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankGridView {
|
DankGridView {
|
||||||
id: appGrid
|
id: appGrid
|
||||||
|
|
||||||
|
property int currentIndex: appLauncher.selectedIndex
|
||||||
|
property int columns: 4
|
||||||
|
property bool adaptiveColumns: false
|
||||||
|
property int minCellWidth: 120
|
||||||
|
property int maxCellWidth: 160
|
||||||
|
property int cellPadding: 8
|
||||||
|
property real iconSizeRatio: 0.6
|
||||||
|
property int maxIconSize: 56
|
||||||
|
property int minIconSize: 32
|
||||||
|
property bool hoverUpdatesSelection: false
|
||||||
|
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
||||||
|
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
||||||
|
property int baseCellHeight: baseCellWidth + 20
|
||||||
|
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||||
|
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||||
|
|
||||||
|
signal keyboardNavigationReset()
|
||||||
|
signal itemClicked(int index, var modelData)
|
||||||
|
signal itemHovered(int index)
|
||||||
|
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function ensureVisible(index) {
|
||||||
|
if (index < 0 || index >= count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var itemY = Math.floor(index / actualColumns) * cellHeight;
|
||||||
|
var itemBottom = itemY + cellHeight;
|
||||||
|
if (itemY < contentY)
|
||||||
|
contentY = itemY;
|
||||||
|
else if (itemBottom > contentY + height)
|
||||||
|
contentY = itemBottom - height;
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
visible: appLauncher.viewMode === "grid"
|
visible: appLauncher.viewMode === "grid"
|
||||||
model: appLauncher.model
|
model: appLauncher.model
|
||||||
columns: 4
|
clip: true
|
||||||
adaptiveColumns: false
|
cellWidth: baseCellWidth
|
||||||
currentIndex: appLauncher.selectedIndex
|
cellHeight: baseCellHeight
|
||||||
hoverUpdatesSelection: false
|
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||||
keyboardNavigationActive: appLauncher.keyboardNavigationActive
|
rightMargin: leftMargin
|
||||||
|
focus: true
|
||||||
|
interactive: true
|
||||||
|
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (keyboardNavigationActive)
|
||||||
|
ensureVisible(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
onItemClicked: function(index, modelData) {
|
onItemClicked: function(index, modelData) {
|
||||||
appLauncher.launchApp(modelData);
|
appLauncher.launchApp(modelData);
|
||||||
}
|
}
|
||||||
@@ -419,6 +601,103 @@ PanelWindow {
|
|||||||
onKeyboardNavigationReset: {
|
onKeyboardNavigationReset: {
|
||||||
appLauncher.keyboardNavigationActive = false;
|
appLauncher.keyboardNavigationActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.horizontal: ScrollBar {
|
||||||
|
policy: ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: appGrid.cellWidth - appGrid.cellPadding
|
||||||
|
height: appGrid.cellHeight - appGrid.cellPadding
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
color: appGrid.currentIndex === index ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||||
|
border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
||||||
|
border.width: appGrid.currentIndex === index ? 2 : 1
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio))
|
||||||
|
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
||||||
|
smooth: true
|
||||||
|
asynchronous: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: !iconImg.visible
|
||||||
|
color: Theme.surfaceLight
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.primarySelected
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||||
|
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: appGrid.cellWidth - 12
|
||||||
|
text: model.name || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
z: 10
|
||||||
|
onEntered: {
|
||||||
|
if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive)
|
||||||
|
appGrid.currentIndex = index;
|
||||||
|
|
||||||
|
appGrid.itemHovered(index);
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
appGrid.keyboardNavigationReset();
|
||||||
|
}
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
appGrid.itemClicked(index, model);
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
var globalPos = mapToGlobal(mouse.x, mouse.y);
|
||||||
|
appGrid.itemRightClicked(index, model, globalPos.x, globalPos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ Rectangle {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
DankListView {
|
||||||
id: eventsList
|
id: eventsList
|
||||||
|
|
||||||
anchors.top: headerRow.bottom
|
anchors.top: headerRow.bottom
|
||||||
|
|||||||
@@ -33,46 +33,90 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
// Output Tab - DankFlickable
|
||||||
|
DankFlickable {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 48
|
height: parent.height - 48
|
||||||
visible: audioTab.audioSubTab === 0
|
visible: audioTab.audioSubTab === 0
|
||||||
clip: true
|
clip: true
|
||||||
|
contentHeight: outputColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: outputColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
VolumeControl {
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: volumeComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioDevicesList {
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: outputDevicesComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
// Input Tab - DankFlickable
|
||||||
|
DankFlickable {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - 48
|
height: parent.height - 48
|
||||||
visible: audioTab.audioSubTab === 1
|
visible: audioTab.audioSubTab === 1
|
||||||
clip: true
|
clip: true
|
||||||
|
contentHeight: inputColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: inputColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
MicrophoneControl {
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: microphoneComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioInputDevicesList {
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: inputDevicesComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// Volume Control Component
|
||||||
|
Component {
|
||||||
|
id: volumeComponent
|
||||||
|
VolumeControl {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Microphone Control Component
|
||||||
|
Component {
|
||||||
|
id: microphoneComponent
|
||||||
|
MicrophoneControl {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output Devices Component
|
||||||
|
Component {
|
||||||
|
id: outputDevicesComponent
|
||||||
|
AudioDevicesList {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input Devices Component
|
||||||
|
Component {
|
||||||
|
id: inputDevicesComponent
|
||||||
|
AudioInputDevicesList {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,15 @@ import qs.Widgets
|
|||||||
Column {
|
Column {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var bluetoothContextMenuWindow
|
function findBluetoothContextMenu() {
|
||||||
|
var p = parent;
|
||||||
|
while (p) {
|
||||||
|
if (p.bluetoothContextMenuWindow)
|
||||||
|
return p.bluetoothContextMenuWindow;
|
||||||
|
p = p.parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
@@ -117,10 +125,11 @@ Column {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (bluetoothContextMenuWindow) {
|
var contextMenu = root.findBluetoothContextMenu();
|
||||||
bluetoothContextMenuWindow.deviceData = modelData;
|
if (contextMenu) {
|
||||||
let localPos = btMenuButtonArea.mapToItem(bluetoothContextMenuWindow.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height);
|
contextMenu.deviceData = modelData;
|
||||||
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
|
let localPos = btMenuButtonArea.mapToItem(contextMenu.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height);
|
||||||
|
contextMenu.show(localPos.x, localPos.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,33 +12,39 @@ import qs.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: bluetoothTab
|
id: bluetoothTab
|
||||||
|
|
||||||
ScrollView {
|
property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
contentHeight: mainColumn.height
|
||||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
contentWidth: width
|
||||||
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: mainColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
BluetoothToggle {
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: toggleComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
PairedDevicesList {
|
Loader {
|
||||||
bluetoothContextMenuWindow: bluetoothContextMenuWindow
|
width: parent.width
|
||||||
|
sourceComponent: pairedComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
AvailableDevicesList {
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: availableComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BluetoothContextMenu {
|
BluetoothContextMenu {
|
||||||
id: bluetoothContextMenuWindow
|
id: bluetoothContextMenuWindow
|
||||||
|
|
||||||
parentItem: bluetoothTab
|
parentItem: bluetoothTab
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +63,26 @@ Item {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
Component {
|
||||||
|
id: toggleComponent
|
||||||
|
BluetoothToggle {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: pairedComponent
|
||||||
|
PairedDevicesList {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: availableComponent
|
||||||
|
AvailableDevicesList {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import qs.Modules
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ScrollView {
|
Item {
|
||||||
id: displayTab
|
id: displayTab
|
||||||
|
|
||||||
property var brightnessDebounceTimer
|
property var brightnessDebounceTimer
|
||||||
@@ -16,19 +16,61 @@ ScrollView {
|
|||||||
brightnessDebounceTimer: Timer {
|
brightnessDebounceTimer: Timer {
|
||||||
property int pendingValue: 0
|
property int pendingValue: 0
|
||||||
|
|
||||||
interval: BrightnessService.ddcAvailable ? 500 : 50 // 500ms for slow DDC (i2c), 50ms for fast laptop backlight
|
interval: BrightnessService.ddcAvailable ? 500 : 50
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
|
||||||
BrightnessService.setBrightnessInternal(pendingValue);
|
BrightnessService.setBrightnessInternal(pendingValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clip: true
|
DankFlickable {
|
||||||
Column {
|
anchors.fill: parent
|
||||||
width: parent.width
|
clip: true
|
||||||
spacing: Theme.spacingL
|
contentHeight: mainColumn.height
|
||||||
|
contentWidth: width
|
||||||
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: brightnessComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: settingsComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: nightModeEnableProcess
|
||||||
|
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
|
||||||
|
running: false
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
SettingsData.setNightModeEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: nightModeDisableProcess
|
||||||
|
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
|
||||||
|
running: false
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: brightnessComponent
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
@@ -48,12 +90,10 @@ ScrollView {
|
|||||||
rightIcon: "brightness_high"
|
rightIcon: "brightness_high"
|
||||||
enabled: BrightnessService.brightnessAvailable
|
enabled: BrightnessService.brightnessAvailable
|
||||||
onSliderValueChanged: function(newValue) {
|
onSliderValueChanged: function(newValue) {
|
||||||
|
|
||||||
brightnessDebounceTimer.pendingValue = newValue;
|
brightnessDebounceTimer.pendingValue = newValue;
|
||||||
brightnessDebounceTimer.restart();
|
brightnessDebounceTimer.restart();
|
||||||
}
|
}
|
||||||
onSliderDragFinished: function(finalValue) {
|
onSliderDragFinished: function(finalValue) {
|
||||||
|
|
||||||
brightnessDebounceTimer.stop();
|
brightnessDebounceTimer.stop();
|
||||||
BrightnessService.setBrightnessInternal(finalValue);
|
BrightnessService.setBrightnessInternal(finalValue);
|
||||||
}
|
}
|
||||||
@@ -66,9 +106,11 @@ ScrollView {
|
|||||||
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable
|
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: settingsComponent
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
@@ -110,12 +152,10 @@ ScrollView {
|
|||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: nightModeToggle
|
id: nightModeToggle
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -129,7 +169,6 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -158,12 +197,10 @@ ScrollView {
|
|||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: lightModeToggle
|
id: lightModeToggle
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -177,40 +214,9 @@ ScrollView {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: nightModeEnableProcess
|
|
||||||
|
|
||||||
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
|
|
||||||
running: false
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
|
|
||||||
SettingsData.setNightModeEnabled(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Process {
|
|
||||||
id: nightModeDisableProcess
|
|
||||||
|
|
||||||
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
|
|
||||||
running: false
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode !== 0)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -928,7 +928,7 @@ Item {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
logoutDialog.close()
|
logoutDialog.close()
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "quit", "-s"])
|
NiriService.quit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,144 +2,27 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
ListView {
|
DankListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias count: root.count
|
property alias count: root.count
|
||||||
readonly property real listContentHeight: root.contentHeight
|
property alias listContentHeight: root.contentHeight
|
||||||
readonly property bool atYBeginning: root.contentY === 0
|
|
||||||
property real stableY: 0
|
|
||||||
property bool isUserScrolling: false
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
clip: true
|
clip: true
|
||||||
model: NotificationService.groupedNotifications
|
model: NotificationService.groupedNotifications
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
interactive: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
cacheBuffer: 1000
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
property real momentum: 0
|
|
||||||
onWheel: (event) => {
|
|
||||||
if (event.pixelDelta.y !== 0) {
|
|
||||||
// Touchpad with pixel delta
|
|
||||||
momentum = event.pixelDelta.y * 1.8
|
|
||||||
} else {
|
|
||||||
// Mouse wheel with angle delta
|
|
||||||
momentum = (event.angleDelta.y / 120) * (parent.spacing * 2.5) // ~2.5 items per wheel step
|
|
||||||
}
|
|
||||||
|
|
||||||
let newY = parent.contentY - momentum
|
|
||||||
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
momentum *= 0.92 // Decay for smooth momentum
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMovementStarted: isUserScrolling = true
|
|
||||||
onMovementEnded: {
|
|
||||||
isUserScrolling = false;
|
|
||||||
if (contentY > 40)
|
|
||||||
stableY = contentY;
|
|
||||||
|
|
||||||
}
|
|
||||||
onContentYChanged: {
|
|
||||||
if (!isUserScrolling && visible && parent.visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
|
|
||||||
contentY = stableY;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationEmptyState {
|
NotificationEmptyState {
|
||||||
visible: root.count === 0
|
visible: root.count === 0
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
add: Transition {
|
|
||||||
enabled: !root.isUserScrolling
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "opacity"
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
duration: root.isUserScrolling ? 0 : Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "height"
|
|
||||||
from: 0
|
|
||||||
duration: root.isUserScrolling ? 0 : Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
remove: Transition {
|
|
||||||
SequentialAnimation {
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 50
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "opacity"
|
|
||||||
to: 0
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "height,anchors.topMargin,anchors.bottomMargin"
|
|
||||||
to: 0
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
displaced: Transition {
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "y"
|
|
||||||
duration: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
move: Transition {
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "y"
|
|
||||||
duration: 0
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: NotificationCard {
|
delegate: NotificationCard {
|
||||||
notificationGroup: modelData
|
notificationGroup: modelData
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ Column {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
processListView.captureAnchor();
|
|
||||||
SysMonitorService.setSortBy("name");
|
SysMonitorService.setSortBy("name");
|
||||||
processListView.restoreAnchor();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +88,7 @@ Column {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
processListView.captureAnchor();
|
|
||||||
SysMonitorService.setSortBy("cpu");
|
SysMonitorService.setSortBy("cpu");
|
||||||
processListView.restoreAnchor();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +127,7 @@ Column {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
processListView.captureAnchor();
|
|
||||||
SysMonitorService.setSortBy("memory");
|
SysMonitorService.setSortBy("memory");
|
||||||
processListView.restoreAnchor();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +167,7 @@ Column {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
processListView.captureAnchor();
|
|
||||||
SysMonitorService.setSortBy("pid");
|
SysMonitorService.setSortBy("pid");
|
||||||
processListView.restoreAnchor();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,9 +203,7 @@ Column {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
processListView.captureAnchor();
|
|
||||||
SysMonitorService.toggleSortOrder();
|
SysMonitorService.toggleSortOrder();
|
||||||
processListView.restoreAnchor();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,101 +218,21 @@ Column {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
DankListView {
|
||||||
id: processListView
|
id: processListView
|
||||||
|
|
||||||
property real stableY: 0
|
|
||||||
property bool isUserScrolling: false
|
|
||||||
property bool isScrollBarDragging: false
|
|
||||||
property string keyRoleName: "pid"
|
property string keyRoleName: "pid"
|
||||||
property var _anchorKey: undefined
|
|
||||||
property real _anchorOffset: 0
|
|
||||||
|
|
||||||
function captureAnchor() {
|
|
||||||
const y = contentY + 1;
|
|
||||||
const idx = indexAt(0, y);
|
|
||||||
if (idx < 0 || !model || idx >= model.length)
|
|
||||||
return ;
|
|
||||||
|
|
||||||
_anchorKey = model[idx][keyRoleName];
|
|
||||||
const it = itemAtIndex(idx);
|
|
||||||
_anchorOffset = it ? (y - it.y) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreAnchor() {
|
|
||||||
Qt.callLater(function() {
|
|
||||||
if (_anchorKey === undefined || !model)
|
|
||||||
return ;
|
|
||||||
|
|
||||||
var i = -1;
|
|
||||||
for (var j = 0; j < model.length; ++j) {
|
|
||||||
if (model[j][keyRoleName] === _anchorKey) {
|
|
||||||
i = j;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i < 0)
|
|
||||||
return ;
|
|
||||||
|
|
||||||
positionViewAtIndex(i, ListView.Beginning);
|
|
||||||
const maxY = Math.max(0, contentHeight - height);
|
|
||||||
contentY = Math.max(0, Math.min(maxY, contentY + _anchorOffset - 1));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - columnHeaders.height
|
height: parent.height - columnHeaders.height
|
||||||
clip: true
|
clip: true
|
||||||
spacing: 4
|
spacing: 4
|
||||||
model: SysMonitorService.processes
|
model: SysMonitorService.processes
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
interactive: true
|
|
||||||
flickDeceleration: 1500 // Touch only in Qt 6.9+ // Lower = more momentum, longer scrolling
|
|
||||||
maximumFlickVelocity: 2000 // Touch only in Qt 6.9+ // Higher = faster maximum scroll speed
|
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
|
||||||
pressDelay: 0
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
|
|
||||||
onMovementStarted: isUserScrolling = true
|
|
||||||
onMovementEnded: {
|
|
||||||
isUserScrolling = false;
|
|
||||||
if (contentY > 40)
|
|
||||||
stableY = contentY;
|
|
||||||
}
|
|
||||||
onContentYChanged: {
|
|
||||||
if (!isUserScrolling && !isScrollBarDragging && visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
|
|
||||||
contentY = stableY;
|
|
||||||
}
|
|
||||||
onModelChanged: {
|
|
||||||
if (model && model.length > 0 && !isUserScrolling && stableY > 40)
|
|
||||||
Qt.callLater(function() {
|
|
||||||
contentY = stableY;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: ProcessListItem {
|
delegate: ProcessListItem {
|
||||||
process: modelData
|
process: modelData
|
||||||
contextMenu: root.contextMenu
|
contextMenu: root.contextMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
id: verticalScrollBar
|
|
||||||
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
onPressedChanged: {
|
|
||||||
processListView.isScrollBarDragging = pressed;
|
|
||||||
if (!pressed && processListView.contentY > 40)
|
|
||||||
processListView.stableY = processListView.contentY;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,35 +5,49 @@ import qs.Common
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ScrollView {
|
Item {
|
||||||
id: appearanceTab
|
id: appearanceTab
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness
|
DankFlickable {
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
anchors.fill: parent
|
||||||
WheelHandler {
|
anchors.topMargin: Theme.spacingL
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
anchors.bottomMargin: Theme.spacingXL
|
||||||
onWheel: (event) => {
|
clip: true
|
||||||
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80
|
contentHeight: mainColumn.height
|
||||||
let flickable = appearanceTab.contentItem
|
contentWidth: width
|
||||||
let newY = flickable.contentY - delta
|
mouseWheelSpeed: 20
|
||||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
|
|
||||||
flickable.contentY = newY
|
Column {
|
||||||
event.accepted = true
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: displayComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: transparencyComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: themeComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: systemThemingComponent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentWidth: availableWidth
|
// Display Settings Component
|
||||||
contentHeight: column.implicitHeight + Theme.spacingXL
|
Component {
|
||||||
clip: true
|
id: displayComponent
|
||||||
|
|
||||||
Column {
|
|
||||||
id: column
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
topPadding: Theme.spacingL
|
|
||||||
bottomPadding: Theme.spacingXL
|
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: displaySection.implicitHeight + Theme.spacingL * 2
|
height: displaySection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -273,9 +287,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transparency Settings Component
|
||||||
|
Component {
|
||||||
|
id: transparencyComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -391,9 +409,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme Color Component
|
||||||
|
Component {
|
||||||
|
id: themeComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: themeSection.implicitHeight + Theme.spacingL * 2
|
height: themeSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -759,9 +781,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System App Theming Component
|
||||||
|
Component {
|
||||||
|
id: systemThemingComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: systemThemingSection.implicitHeight + Theme.spacingL * 2
|
height: systemThemingSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -828,9 +854,7 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
|
|||||||
@@ -4,34 +4,44 @@ import Quickshell.Widgets
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ScrollView {
|
Item {
|
||||||
id: launcherTab
|
id: launcherTab
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness
|
DankFlickable {
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
anchors.fill: parent
|
||||||
WheelHandler {
|
anchors.topMargin: Theme.spacingL
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
anchors.bottomMargin: Theme.spacingXL
|
||||||
onWheel: (event) => {
|
clip: true
|
||||||
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80
|
contentHeight: mainColumn.height
|
||||||
let flickable = launcherTab.contentItem
|
contentWidth: width
|
||||||
let newY = flickable.contentY - delta
|
mouseWheelSpeed: 20
|
||||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
|
|
||||||
flickable.contentY = newY
|
Column {
|
||||||
event.accepted = true
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: appLauncherComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: dockComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: recentlyUsedComponent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentHeight: column.implicitHeight + Theme.spacingXL
|
// App Launcher Component
|
||||||
clip: true
|
Component {
|
||||||
|
id: appLauncherComponent
|
||||||
Column {
|
|
||||||
id: column
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
topPadding: Theme.spacingL
|
|
||||||
bottomPadding: Theme.spacingXL
|
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: appLauncherSection.implicitHeight + Theme.spacingL * 2
|
height: appLauncherSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -159,9 +169,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dock Component
|
||||||
|
Component {
|
||||||
|
id: dockComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: dockSection.implicitHeight + Theme.spacingL * 2
|
height: dockSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -262,7 +276,12 @@ ScrollView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recently Used Apps Component
|
||||||
|
Component {
|
||||||
|
id: recentlyUsedComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
|
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -474,9 +493,6 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,35 +7,47 @@ import qs.Modals
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ScrollView {
|
Item {
|
||||||
id: personalizationTab
|
id: personalizationTab
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: (event) => {
|
|
||||||
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80
|
|
||||||
let flickable = personalizationTab.contentItem
|
|
||||||
let newY = flickable.contentY - delta
|
|
||||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
|
|
||||||
flickable.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias profileBrowser: profileBrowserLoader.item
|
property alias profileBrowser: profileBrowserLoader.item
|
||||||
property alias wallpaperBrowser: wallpaperBrowserLoader.item
|
property alias wallpaperBrowser: wallpaperBrowserLoader.item
|
||||||
|
|
||||||
contentHeight: column.implicitHeight
|
DankFlickable {
|
||||||
clip: true
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
Column {
|
anchors.bottomMargin: Theme.spacingXL
|
||||||
id: column
|
clip: true
|
||||||
|
contentHeight: mainColumn.height
|
||||||
width: parent.width
|
contentWidth: width
|
||||||
spacing: Theme.spacingXL
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: profileComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: wallpaperComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: dynamicThemeComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile Image Component
|
||||||
|
Component {
|
||||||
|
id: profileComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: profileSection.implicitHeight + Theme.spacingL * 2
|
height: profileSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -294,9 +306,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallpaper Component
|
||||||
|
Component {
|
||||||
|
id: wallpaperComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: wallpaperSection.implicitHeight + Theme.spacingL * 2
|
height: wallpaperSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -491,9 +507,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic Theme Component
|
||||||
|
Component {
|
||||||
|
id: dynamicThemeComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
|
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -568,9 +588,7 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
|
|||||||
@@ -3,32 +3,39 @@ import QtQuick.Controls
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ScrollView {
|
Item {
|
||||||
id: timeWeatherTab
|
id: timeWeatherTab
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness
|
DankFlickable {
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
anchors.fill: parent
|
||||||
WheelHandler {
|
anchors.topMargin: Theme.spacingL
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
anchors.bottomMargin: Theme.spacingXL
|
||||||
onWheel: (event) => {
|
clip: true
|
||||||
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80
|
contentHeight: mainColumn.height
|
||||||
let flickable = timeWeatherTab.contentItem
|
contentWidth: width
|
||||||
let newY = flickable.contentY - delta
|
mouseWheelSpeed: 20
|
||||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
|
|
||||||
flickable.contentY = newY
|
Column {
|
||||||
event.accepted = true
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: timeComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: weatherComponent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentHeight: column.implicitHeight
|
// Time Format Component
|
||||||
clip: true
|
Component {
|
||||||
|
id: timeComponent
|
||||||
Column {
|
|
||||||
id: column
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXL
|
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: timeSection.implicitHeight + Theme.spacingL * 2
|
height: timeSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -76,9 +83,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather Component
|
||||||
|
Component {
|
||||||
|
id: weatherComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: weatherSection.implicitHeight + Theme.spacingL * 2
|
height: weatherSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -171,9 +182,6 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,9 @@ import QtQuick.Controls
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ScrollView {
|
Item {
|
||||||
id: widgetsTab
|
id: widgetsTab
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: (event) => {
|
|
||||||
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80
|
|
||||||
let flickable = widgetsTab.contentItem
|
|
||||||
let newY = flickable.contentY - delta
|
|
||||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
|
|
||||||
flickable.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var baseWidgetDefinitions: [{
|
property var baseWidgetDefinitions: [{
|
||||||
"id": "launcherButton",
|
"id": "launcherButton",
|
||||||
"text": "App Launcher",
|
"text": "App Launcher",
|
||||||
@@ -305,8 +291,6 @@ ScrollView {
|
|||||||
return widgets;
|
return widgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
contentHeight: column.implicitHeight + Theme.spacingXL
|
|
||||||
clip: true
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!SettingsData.topBarLeftWidgets || SettingsData.topBarLeftWidgets.length === 0)
|
if (!SettingsData.topBarLeftWidgets || SettingsData.topBarLeftWidgets.length === 0)
|
||||||
SettingsData.setTopBarLeftWidgets(defaultLeftWidgets);
|
SettingsData.setTopBarLeftWidgets(defaultLeftWidgets);
|
||||||
@@ -347,14 +331,46 @@ ScrollView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
DankFlickable {
|
||||||
id: column
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
width: parent.width
|
anchors.bottomMargin: Theme.spacingXL
|
||||||
spacing: Theme.spacingXL
|
clip: true
|
||||||
topPadding: Theme.spacingL
|
contentHeight: mainColumn.height
|
||||||
bottomPadding: Theme.spacingXL
|
contentWidth: width
|
||||||
|
mouseWheelSpeed: 20
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: headerComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: messageComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: sectionsComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
width: parent.width
|
||||||
|
sourceComponent: workspaceComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header Component
|
||||||
|
Component {
|
||||||
|
id: headerComponent
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
@@ -406,7 +422,6 @@ ScrollView {
|
|||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -427,7 +442,6 @@ ScrollView {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on border.color {
|
Behavior on border.color {
|
||||||
@@ -435,13 +449,15 @@ ScrollView {
|
|||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Theme.standardEasing
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message Component
|
||||||
|
Component {
|
||||||
|
id: messageComponent
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: messageText.contentHeight + Theme.spacingM * 2
|
height: messageText.contentHeight + Theme.spacingM * 2
|
||||||
@@ -449,9 +465,6 @@ ScrollView {
|
|||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
visible: true
|
|
||||||
opacity: 1
|
|
||||||
z: 1
|
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: messageText
|
id: messageText
|
||||||
@@ -463,9 +476,13 @@ ScrollView {
|
|||||||
width: parent.width - Theme.spacingM * 2
|
width: parent.width - Theme.spacingM * 2
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sections Component
|
||||||
|
Component {
|
||||||
|
id: sectionsComponent
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
@@ -568,9 +585,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspace Settings Component
|
||||||
|
Component {
|
||||||
|
id: workspaceComponent
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: workspaceSection.implicitHeight + Theme.spacingL * 2
|
height: workspaceSection.implicitHeight + Theme.spacingL * 2
|
||||||
@@ -604,7 +625,6 @@ ScrollView {
|
|||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DankToggle {
|
DankToggle {
|
||||||
@@ -626,44 +646,10 @@ ScrollView {
|
|||||||
return SettingsData.setShowWorkspacePadding(checked);
|
return SettingsData.setShowWorkspacePadding(checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: tooltipText.contentWidth + Theme.spacingM * 2
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
visible: resetArea.containsMouse
|
|
||||||
opacity: resetArea.containsMouse ? 1 : 0
|
|
||||||
y: column.y + 48
|
|
||||||
x: parent.width - width - Theme.spacingM
|
|
||||||
z: 100
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Reset widget layout to defaults"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankWidgetSelectionPopup {
|
DankWidgetSelectionPopup {
|
||||||
id: widgetSelectionPopup
|
id: widgetSelectionPopup
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import Quickshell.Services.SystemTray
|
import Quickshell.Services.SystemTray
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property var parentWindow: null
|
||||||
|
property var parentScreen: null
|
||||||
|
|
||||||
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
|
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
|
||||||
|
|
||||||
signal menuRequested(var menu, var item, real x, real y)
|
|
||||||
|
|
||||||
width: calculatedWidth
|
width: calculatedWidth
|
||||||
height: 30
|
height: 30
|
||||||
@@ -84,16 +87,19 @@ Rectangle {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: (mouse) => {
|
onClicked: (mouse) => {
|
||||||
if (!trayItem)
|
if (!trayItem)
|
||||||
return ;
|
return
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
if (!trayItem.onlyMenu)
|
|
||||||
trayItem.activate();
|
|
||||||
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
if (trayItem && trayItem.hasMenu)
|
|
||||||
root.menuRequested(null, trayItem, mouse.x, mouse.y);
|
|
||||||
|
|
||||||
|
if (trayItem.hasMenu) {
|
||||||
|
var globalPos = mapToGlobal(0, 0)
|
||||||
|
var currentScreen = parentScreen || Screen
|
||||||
|
var screenX = currentScreen.x || 0
|
||||||
|
var relativeX = globalPos.x - screenX
|
||||||
|
menuAnchor.menu = trayItem.menu
|
||||||
|
menuAnchor.anchor.window = parentWindow
|
||||||
|
menuAnchor.anchor.rect = Qt.rect(relativeX, Theme.barHeight + Theme.spacingS, parent.width, 1)
|
||||||
|
menuAnchor.open()
|
||||||
|
} else if (mouse.button === Qt.LeftButton) {
|
||||||
|
trayItem.activate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,4 +110,8 @@ Rectangle {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QsMenuAnchor {
|
||||||
|
id: menuAnchor
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
id: systemTrayComponent
|
||||||
|
|
||||||
SystemTrayBar {
|
SystemTrayBar {
|
||||||
onMenuRequested: (menu, item, x, y) => {
|
parentWindow: root
|
||||||
systemTrayContextMenu.currentTrayMenu = menu;
|
parentScreen: root.screen
|
||||||
systemTrayContextMenu.currentTrayItem = item;
|
|
||||||
systemTrayContextMenu.contextMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
|
|
||||||
systemTrayContextMenu.contextMenuY = Theme.barHeight - Theme.spacingXS;
|
|
||||||
systemTrayContextMenu.showContextMenu = true;
|
|
||||||
if (menu) {
|
|
||||||
menu.menuVisible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ Rectangle {
|
|||||||
enabled: !isPlaceholder
|
enabled: !isPlaceholder
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!isPlaceholder)
|
if (!isPlaceholder)
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]);
|
NiriService.switchToWorkspace(modelData - 1);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
README.md
107
README.md
@@ -23,24 +23,31 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
### Application Launcher
|
### Application Launcher
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Spotlight Launcher" />
|
<img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Spotlight Launcher" />
|
||||||
|
|
||||||
### System Monitor
|
### System Monitor
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/b3c817ec-734d-4974-929f-2d11a1065349" width="600" alt="System Monitor" />
|
<img src="https://github.com/user-attachments/assets/b3c817ec-734d-4974-929f-2d11a1065349" width="600" alt="System Monitor" />
|
||||||
|
|
||||||
### Widget Customization
|
### Widget Customization
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/903f7c60-146f-4fb3-a75d-a4823828f298" width="500" alt="Widget Customization" />
|
<img src="https://github.com/user-attachments/assets/903f7c60-146f-4fb3-a75d-a4823828f298" width="500" alt="Widget Customization" />
|
||||||
|
|
||||||
### Lock Screen
|
### Lock Screen
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/3fa07de2-c1b0-4e57-8f25-3830ac6baf4f" width="600" alt="Lock Screen" />
|
<img src="https://github.com/user-attachments/assets/3fa07de2-c1b0-4e57-8f25-3830ac6baf4f" width="600" alt="Lock Screen" />
|
||||||
|
|
||||||
### Dynamic Theming
|
### Dynamic Theming
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/1994e616-f9d9-424a-9f60-6f06708bf12e" width="700" alt="Auto Theme" />
|
<img src="https://github.com/user-attachments/assets/1994e616-f9d9-424a-9f60-6f06708bf12e" width="700" alt="Auto Theme" />
|
||||||
|
|
||||||
### Notification Center
|
### Notification Center
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/07cbde9a-0242-4989-9f97-5765c6458c85" width="350" alt="Notification Center" />
|
<img src="https://github.com/user-attachments/assets/07cbde9a-0242-4989-9f97-5765c6458c85" width="350" alt="Notification Center" />
|
||||||
|
|
||||||
### Dock
|
### Dock
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/e6999daf-f7bf-4329-98fa-0ce4f0e7219c" width="400" alt="Dock" />
|
<img src="https://github.com/user-attachments/assets/e6999daf-f7bf-4329-98fa-0ce4f0e7219c" width="400" alt="Dock" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +57,7 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
|
|||||||
## What's Inside
|
## What's Inside
|
||||||
|
|
||||||
**Core Widgets:**
|
**Core Widgets:**
|
||||||
|
|
||||||
- **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged.
|
- **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged.
|
||||||
- **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps.
|
- **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps.
|
||||||
- **Workspace Switcher** Dynamically resizing niri workspace switcher.
|
- **Workspace Switcher** Dynamically resizing niri workspace switcher.
|
||||||
@@ -71,6 +79,7 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
|
|||||||
- **Lock Screen** Using quickshell's WlSessionLock
|
- **Lock Screen** Using quickshell's WlSessionLock
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
- Dynamic wallpaper-based theming with matugen integration
|
- Dynamic wallpaper-based theming with matugen integration
|
||||||
- Numerous IPCs to trigger actions and open various modals.
|
- Numerous IPCs to trigger actions and open various modals.
|
||||||
- Calendar integration with [khal](https://github.com/pimutils/khal)
|
- Calendar integration with [khal](https://github.com/pimutils/khal)
|
||||||
@@ -83,14 +92,15 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
|
|||||||
|
|
||||||
### Quick Start
|
### Quick Start
|
||||||
|
|
||||||
*If you do not already have niri, see [#]
|
\*If you do not already have niri, see [#]
|
||||||
|
|
||||||
**Dependencies:**
|
**Dependencies:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Arch Linux
|
# Arch Linux
|
||||||
paru -S quickshell-git ttf-material-symbols-variable-git inter-font ttf-fira-code
|
paru -S quickshell-git ttf-material-symbols-variable-git inter-font ttf-fira-code
|
||||||
|
|
||||||
# Fedora
|
# Fedora
|
||||||
sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git rsms-inter-fonts fira-code-fonts
|
sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git rsms-inter-fonts fira-code-fonts
|
||||||
# Install icon fonts manually
|
# Install icon fonts manually
|
||||||
mkdir -p ~/.local/share/fonts
|
mkdir -p ~/.local/share/fonts
|
||||||
@@ -99,6 +109,7 @@ fc-cache -f
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Get the shell:**
|
**Get the shell:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Arch linux available via AUR
|
# Arch linux available via AUR
|
||||||
paru -S dankmaterialshell-git
|
paru -S dankmaterialshell-git
|
||||||
@@ -114,6 +125,7 @@ qs -c DankMaterialShell
|
|||||||
<details><summary>Font Installation</summary>
|
<details><summary>Font Installation</summary>
|
||||||
|
|
||||||
**Material Symbols (Required):**
|
**Material Symbols (Required):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Manual installation
|
# Manual installation
|
||||||
mkdir -p ~/.local/share/fonts
|
mkdir -p ~/.local/share/fonts
|
||||||
@@ -125,6 +137,7 @@ paru -S ttf-material-symbols-variable-git
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Typography (Recommended):**
|
**Typography (Recommended):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Inter Variable Font
|
# Inter Variable Font
|
||||||
curl -L "https://github.com/rsms/inter/releases/download/v4.0/Inter-4.0.zip" -o /tmp/Inter.zip
|
curl -L "https://github.com/rsms/inter/releases/download/v4.0/Inter-4.0.zip" -o /tmp/Inter.zip
|
||||||
@@ -142,23 +155,24 @@ rm /tmp/FiraCode.zip && fc-cache -f
|
|||||||
<details><summary>Optional Features</summary>
|
<details><summary>Optional Features</summary>
|
||||||
|
|
||||||
**Enhanced Functionality:**
|
**Enhanced Functionality:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Arch Linux
|
# Arch Linux
|
||||||
pacman -S cava wl-clipboard cliphist ddcutil brightnessctl qt5ct qt6ct
|
pacman -S cava wl-clipboard cliphist ddcutil brightnessctl
|
||||||
paru -S matugen
|
paru -S matugen
|
||||||
|
|
||||||
# Fedora
|
# Fedora
|
||||||
sudo dnf install cava wl-clipboard ddcutil brightnessctl qt5ct qt6ct
|
sudo dnf install cava wl-clipboard ddcutil brightnessctl
|
||||||
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist
|
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist
|
||||||
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
|
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
|
||||||
```
|
```
|
||||||
|
|
||||||
**What you get:**
|
**What you get:**
|
||||||
|
|
||||||
- `matugen`: Wallpaper-based dynamic theming
|
- `matugen`: Wallpaper-based dynamic theming
|
||||||
- `ddcutil`: External monitor brightness control
|
- `ddcutil`: External monitor brightness control
|
||||||
- `brightnessctl`: Laptop display brightness
|
- `brightnessctl`: Laptop display brightness
|
||||||
- `wl-clipboard`: Required for copying various elements to clipboard.
|
- `wl-clipboard`: Required for copying various elements to clipboard.
|
||||||
- `qt5ct/qt6ct`: Qt application theming
|
|
||||||
- `cava`: Audio visualizer
|
- `cava`: Audio visualizer
|
||||||
- `cliphist`: Clipboard history
|
- `cliphist`: Clipboard history
|
||||||
|
|
||||||
@@ -219,7 +233,7 @@ binds {
|
|||||||
}
|
}
|
||||||
XF86MonBrightnessDown allow-when-locked=true {
|
XF86MonBrightnessDown allow-when-locked=true {
|
||||||
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5";
|
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -232,7 +246,7 @@ Control everything from the command line, or via keybinds:
|
|||||||
qs -c DankMaterialShell ipc call audio setvolume 50
|
qs -c DankMaterialShell ipc call audio setvolume 50
|
||||||
qs -c DankMaterialShell ipc call audio mute
|
qs -c DankMaterialShell ipc call audio mute
|
||||||
|
|
||||||
# Launch applications
|
# Launch applications
|
||||||
qs -c DankMaterialShell ipc call spotlight toggle
|
qs -c DankMaterialShell ipc call spotlight toggle
|
||||||
qs -c DankMaterialShell ipc call processlist toggle
|
qs -c DankMaterialShell ipc call processlist toggle
|
||||||
|
|
||||||
@@ -250,54 +264,98 @@ qs -c DankMaterialShell ipc call mpris next
|
|||||||
|
|
||||||
### System App Integration
|
### System App Integration
|
||||||
|
|
||||||
|
There's two toggles in the appearance section of settings, for GTK and QT apps.
|
||||||
|
|
||||||
|
These settings will override some local GTK and QT configuration files, you can still integrate auto-theming if you do not wish DankShell to mess with your QTCT/GTK files.
|
||||||
|
|
||||||
|
No matter what when matugen is enabled the files will be created on wallpaper changes:
|
||||||
|
|
||||||
|
- ~/.config/gtk-3.0/dank-colors.css
|
||||||
|
- ~/.config/gtk-4.0/dank-colors.css
|
||||||
|
- ~/.config/qt6ct/colors/matugen.conf
|
||||||
|
- ~/.config/qt5ct/colors/matugen.conf
|
||||||
|
|
||||||
|
If you do not like our theme path, you can integrate this with other GTK themes, matugen themes, etc.
|
||||||
|
|
||||||
**GTK Apps:**
|
**GTK Apps:**
|
||||||
Install [Colloid](https://github.com/vinceliuice/Colloid-gtk-theme) or similar Material theme:
|
|
||||||
|
1. Install [Colloid](https://github.com/vinceliuice/Colloid-gtk-theme)
|
||||||
|
|
||||||
|
Colloid is a hard requirement for the auto-theming because of how it integrates with colloid css files, however you can integrate auto-theming with other themes, you just have to do it manually (so leave the toggle OFF in settings)
|
||||||
|
|
||||||
|
It will still create `~/.config/gtk-3.0/4.0/dank-colors.css` on theme updates, these you can import into other compatible GTK themes.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Some default install settings for colloid
|
||||||
./install.sh -s standard -l --tweaks normal
|
./install.sh -s standard -l --tweaks normal
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure in `~/.config/gtk-3.0/settings.ini`:
|
Configure in `~/.config/gtk-3.0/settings.ini` and `~/.config/gtk-4.0/settings.ini`:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Settings]
|
[Settings]
|
||||||
gtk-theme-name=Colloid
|
gtk-theme-name=Colloid
|
||||||
```
|
```
|
||||||
|
|
||||||
**Qt Apps:**
|
**Qt Apps:**
|
||||||
```bash
|
|
||||||
# Install Breeze
|
|
||||||
pacman -S breeze breeze5 # Arch
|
|
||||||
sudo dnf install breeze # Fedora
|
|
||||||
|
|
||||||
# Configure qt5ct/qt6ct
|
You have **two** paths for QT theming, first path is to use **gtk3**. To do that, add the following to your niri config.
|
||||||
echo 'style=Breeze' >> ~/.config/qt5ct/qt5ct.conf
|
|
||||||
|
```kdl
|
||||||
|
environment {
|
||||||
|
// Add to existing environment block
|
||||||
|
QT_QPA_PLATFORMTHEME "gtk3"
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Dynamic Theming:**
|
**Done** - if you're not happy with this and wish to use Breeze or another QT theme then continue on.
|
||||||
Enable wallpaper-based theming in **Settings → Appearance → System App Theming** after installing matugen.
|
|
||||||
|
1. Install qt6ct and qt5ct
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arch
|
||||||
|
pacman -S qt5ct qt6ct
|
||||||
|
# Fedora
|
||||||
|
dnf install qt5ct qt6ct
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Configure Environment in niri
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
// Add to existing environment block
|
||||||
|
QT_QPA_PLATFORMTHEME "qt5ct"
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6 "qt6ct"
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll have to restart your session for themes to take effect.
|
||||||
|
|
||||||
### Terminal Integration
|
### Terminal Integration
|
||||||
|
|
||||||
**Ghostty users** can add automatic color theming:
|
**Ghostty users** can add automatic color theming:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config
|
echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config
|
||||||
```
|
```
|
||||||
|
|
||||||
## Calendar Setup
|
## Calendar Setup
|
||||||
|
|
||||||
Sync your Google Calendar for dashboard integration:
|
Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration:
|
||||||
|
|
||||||
<details><summary>Configuration Steps</summary>
|
<details><summary>Configuration Steps</summary>
|
||||||
|
|
||||||
**Install dependencies:**
|
**Install dependencies:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Arch
|
# Arch
|
||||||
pacman -S vdirsyncer khal python-aiohttp-oauthlib
|
pacman -S vdirsyncer khal python-aiohttp-oauthlib
|
||||||
|
|
||||||
# Fedora
|
# Fedora
|
||||||
sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib
|
sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib
|
||||||
```
|
```
|
||||||
|
|
||||||
**Configure vdirsyncer** (`~/.vdirsyncer/config`):
|
**Configure vdirsyncer** (`~/.vdirsyncer/config`):
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[general]
|
[general]
|
||||||
status_path = "~/.calendars/status"
|
status_path = "~/.calendars/status"
|
||||||
@@ -322,6 +380,7 @@ fileext = ".ics"
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Setup sync:**
|
**Setup sync:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
vdirsyncer sync
|
vdirsyncer sync
|
||||||
khal configure
|
khal configure
|
||||||
@@ -338,8 +397,9 @@ crontab -e
|
|||||||
All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal.
|
All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal.
|
||||||
|
|
||||||
**Key configuration areas:**
|
**Key configuration areas:**
|
||||||
|
|
||||||
- Widget positioning and behavior
|
- Widget positioning and behavior
|
||||||
- Theme and color preferences
|
- Theme and color preferences
|
||||||
- Time format, weather units and location
|
- Time format, weather units and location
|
||||||
- Light/Dark modes
|
- Light/Dark modes
|
||||||
- Wallpaper and Profile picture
|
- Wallpaper and Profile picture
|
||||||
@@ -348,12 +408,14 @@ All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Common issues:**
|
**Common issues:**
|
||||||
|
|
||||||
- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material`
|
- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material`
|
||||||
- **No dynamic theming:** Install matugen and enable in settings
|
- **No dynamic theming:** Install matugen and enable in settings
|
||||||
- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME
|
- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME
|
||||||
- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity
|
- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity
|
||||||
|
|
||||||
**Getting help:**
|
**Getting help:**
|
||||||
|
|
||||||
- Check the [issues](https://github.com/bbedward/DankMaterialShell/issues) for known problems
|
- Check the [issues](https://github.com/bbedward/DankMaterialShell/issues) for known problems
|
||||||
- Share logs from `qs -c DankMaterialShell` for debugging
|
- Share logs from `qs -c DankMaterialShell` for debugging
|
||||||
- Join the niri community for compositor-specific questions
|
- Join the niri community for compositor-specific questions
|
||||||
@@ -363,6 +425,7 @@ All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or
|
|||||||
DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, theme improvements, or documentation updates - all help is appreciated.
|
DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, theme improvements, or documentation updates - all help is appreciated.
|
||||||
|
|
||||||
**Areas that need attention:**
|
**Areas that need attention:**
|
||||||
|
|
||||||
- More widget options and customization
|
- More widget options and customization
|
||||||
- Additional compositor compatibility
|
- Additional compositor compatibility
|
||||||
- Performance optimizations
|
- Performance optimizations
|
||||||
@@ -373,4 +436,4 @@ DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, t
|
|||||||
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
|
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
|
||||||
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
|
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
|
||||||
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
||||||
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
|
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
|
||||||
|
|||||||
@@ -7,371 +7,293 @@ import Quickshell.Io
|
|||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Workspace management
|
// Workspace management
|
||||||
|
property var workspaces: ({})
|
||||||
property var allWorkspaces: []
|
property var allWorkspaces: []
|
||||||
property int focusedWorkspaceIndex: 0
|
property int focusedWorkspaceIndex: 0
|
||||||
property string focusedWorkspaceId: ""
|
property string focusedWorkspaceId: ""
|
||||||
property var currentOutputWorkspaces: []
|
property var currentOutputWorkspaces: []
|
||||||
property string currentOutput: ""
|
property string currentOutput: ""
|
||||||
|
|
||||||
// Window management
|
// Window management
|
||||||
property var windows: []
|
property var windows: []
|
||||||
property int focusedWindowIndex: -1
|
property int focusedWindowIndex: -1
|
||||||
property string focusedWindowTitle: "(No active window)"
|
property string focusedWindowTitle: "(No active window)"
|
||||||
property string focusedWindowId: ""
|
property string focusedWindowId: ""
|
||||||
|
|
||||||
// Overview state
|
// Overview state
|
||||||
property bool inOverview: false
|
property bool inOverview: false
|
||||||
|
|
||||||
signal windowOpenedOrChanged(var windowData)
|
signal windowOpenedOrChanged(var windowData)
|
||||||
|
|
||||||
// Feature availability
|
// Feature availability
|
||||||
property bool niriAvailable: false
|
property bool niriAvailable: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
||||||
console.log("NiriService: Component.onCompleted - initializing service")
|
|
||||||
checkNiriAvailability()
|
Component.onCompleted: checkNiriAvailability()
|
||||||
}
|
|
||||||
|
|
||||||
// Check if niri is available
|
|
||||||
Process {
|
Process {
|
||||||
id: niriCheck
|
id: niriCheck
|
||||||
command: ["which", "niri"]
|
command: ["test", "-S", root.socketPath]
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
onExited: exitCode => {
|
||||||
root.niriAvailable = exitCode === 0
|
root.niriAvailable = exitCode === 0;
|
||||||
if (root.niriAvailable) {
|
if (root.niriAvailable) {
|
||||||
console.log("NiriService: niri found, starting event stream and loading initial data")
|
eventStreamSocket.connected = true;
|
||||||
eventStreamProcess.running = true
|
|
||||||
loadInitialWorkspaceData()
|
|
||||||
} else {
|
|
||||||
console.log("NiriService: niri not found, workspace features disabled")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkNiriAvailability() {
|
function checkNiriAvailability() {
|
||||||
niriCheck.running = true
|
niriCheck.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initial workspace data
|
Socket {
|
||||||
Process {
|
id: eventStreamSocket
|
||||||
id: initialDataQuery
|
path: root.socketPath
|
||||||
command: ["niri", "msg", "-j", "workspaces"]
|
connected: false
|
||||||
running: false
|
|
||||||
|
onConnectionStateChanged: {
|
||||||
stdout: StdioCollector {
|
if (connected) {
|
||||||
onStreamFinished: {
|
write('"EventStream"\n');
|
||||||
if (text && text.trim()) {
|
|
||||||
try {
|
|
||||||
console.log("NiriService: Loaded initial workspace data")
|
|
||||||
const workspaces = JSON.parse(text.trim())
|
|
||||||
// Initial query returns array directly, event stream wraps it in WorkspacesChanged
|
|
||||||
handleWorkspacesChanged({ workspaces: workspaces })
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("NiriService: Failed to parse initial workspace data:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
parser: SplitParser {
|
||||||
// Load initial windows data
|
onRead: line => {
|
||||||
Process {
|
|
||||||
id: initialWindowsQuery
|
|
||||||
command: ["niri", "msg", "-j", "windows"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text && text.trim()) {
|
|
||||||
try {
|
|
||||||
const windowsData = JSON.parse(text.trim())
|
|
||||||
if (windowsData && windowsData.windows) {
|
|
||||||
handleWindowsChanged(windowsData)
|
|
||||||
console.log("NiriService: Loaded", windowsData.windows.length, "initial windows")
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("NiriService: Failed to parse initial windows data:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load initial focused window data
|
|
||||||
Process {
|
|
||||||
id: initialFocusedWindowQuery
|
|
||||||
command: ["niri", "msg", "-j", "focused-window"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
if (text && text.trim()) {
|
|
||||||
try {
|
|
||||||
const focusedData = JSON.parse(text.trim())
|
|
||||||
if (focusedData && focusedData.id) {
|
|
||||||
handleWindowFocusChanged({ id: focusedData.id })
|
|
||||||
console.log("NiriService: Loaded initial focused window:", focusedData.id)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("NiriService: Failed to parse initial focused window data:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadInitialWorkspaceData() {
|
|
||||||
console.log("NiriService: Loading initial workspace data...")
|
|
||||||
initialDataQuery.running = true
|
|
||||||
initialWindowsQuery.running = true
|
|
||||||
initialFocusedWindowQuery.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event stream for real-time updates
|
|
||||||
Process {
|
|
||||||
id: eventStreamProcess
|
|
||||||
command: ["niri", "msg", "-j", "event-stream"]
|
|
||||||
running: false // Will be enabled after niri check
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
onRead: data => {
|
|
||||||
try {
|
try {
|
||||||
const event = JSON.parse(data.trim())
|
const event = JSON.parse(line);
|
||||||
handleNiriEvent(event)
|
handleNiriEvent(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("NiriService: Failed to parse event:", data, e)
|
console.warn("NiriService: Failed to parse event:", line, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
if (exitCode !== 0 && root.niriAvailable) {
|
|
||||||
console.warn("NiriService: Event stream exited with code", exitCode, "restarting immediately")
|
|
||||||
eventStreamProcess.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Socket {
|
||||||
|
id: requestSocket
|
||||||
|
path: root.socketPath
|
||||||
|
connected: root.niriAvailable
|
||||||
|
}
|
||||||
|
|
||||||
function handleNiriEvent(event) {
|
function handleNiriEvent(event) {
|
||||||
if (event.WorkspacesChanged) {
|
if (event.WorkspacesChanged) {
|
||||||
handleWorkspacesChanged(event.WorkspacesChanged)
|
handleWorkspacesChanged(event.WorkspacesChanged);
|
||||||
} else if (event.WorkspaceActivated) {
|
} else if (event.WorkspaceActivated) {
|
||||||
handleWorkspaceActivated(event.WorkspaceActivated)
|
handleWorkspaceActivated(event.WorkspaceActivated);
|
||||||
} else if (event.WindowsChanged) {
|
} else if (event.WindowsChanged) {
|
||||||
handleWindowsChanged(event.WindowsChanged)
|
handleWindowsChanged(event.WindowsChanged);
|
||||||
} else if (event.WindowClosed) {
|
} else if (event.WindowClosed) {
|
||||||
handleWindowClosed(event.WindowClosed)
|
handleWindowClosed(event.WindowClosed);
|
||||||
} else if (event.WindowFocusChanged) {
|
} else if (event.WindowFocusChanged) {
|
||||||
handleWindowFocusChanged(event.WindowFocusChanged)
|
handleWindowFocusChanged(event.WindowFocusChanged);
|
||||||
} else if (event.WindowOpenedOrChanged) {
|
} else if (event.WindowOpenedOrChanged) {
|
||||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
|
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
|
||||||
} else if (event.OverviewOpenedOrClosed) {
|
} else if (event.OverviewOpenedOrClosed) {
|
||||||
handleOverviewChanged(event.OverviewOpenedOrClosed)
|
handleOverviewChanged(event.OverviewOpenedOrClosed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkspacesChanged(data) {
|
function handleWorkspacesChanged(data) {
|
||||||
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx)
|
const workspaces = {};
|
||||||
|
|
||||||
// Update focused workspace
|
for (const ws of data.workspaces) {
|
||||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
|
workspaces[ws.id] = ws;
|
||||||
if (focusedWorkspaceIndex >= 0) {
|
|
||||||
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
|
|
||||||
focusedWorkspaceId = focusedWs.id
|
|
||||||
currentOutput = focusedWs.output || ""
|
|
||||||
} else {
|
|
||||||
focusedWorkspaceIndex = 0
|
|
||||||
focusedWorkspaceId = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentOutputWorkspaces()
|
root.workspaces = workspaces;
|
||||||
|
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx);
|
||||||
|
|
||||||
|
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused);
|
||||||
|
if (focusedWorkspaceIndex >= 0) {
|
||||||
|
var focusedWs = allWorkspaces[focusedWorkspaceIndex];
|
||||||
|
focusedWorkspaceId = focusedWs.id;
|
||||||
|
currentOutput = focusedWs.output || "";
|
||||||
|
} else {
|
||||||
|
focusedWorkspaceIndex = 0;
|
||||||
|
focusedWorkspaceId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentOutputWorkspaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkspaceActivated(data) {
|
function handleWorkspaceActivated(data) {
|
||||||
// Update focused workspace
|
const ws = root.workspaces[data.id];
|
||||||
focusedWorkspaceId = data.id
|
if (!ws)
|
||||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id)
|
return;
|
||||||
|
const output = ws.output;
|
||||||
if (focusedWorkspaceIndex >= 0) {
|
|
||||||
var activatedWs = allWorkspaces[focusedWorkspaceIndex]
|
for (const id in root.workspaces) {
|
||||||
|
const workspace = root.workspaces[id];
|
||||||
// Update workspace states properly
|
const got_activated = workspace.id === data.id;
|
||||||
// First, deactivate all workspaces on this output
|
|
||||||
for (var i = 0; i < allWorkspaces.length; i++) {
|
if (workspace.output === output) {
|
||||||
if (allWorkspaces[i].output === activatedWs.output) {
|
workspace.is_active = got_activated;
|
||||||
allWorkspaces[i].is_active = false
|
}
|
||||||
allWorkspaces[i].is_focused = false
|
|
||||||
}
|
if (data.focused) {
|
||||||
|
workspace.is_focused = got_activated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then activate the new workspace
|
|
||||||
allWorkspaces[focusedWorkspaceIndex].is_active = true
|
|
||||||
allWorkspaces[focusedWorkspaceIndex].is_focused = data.focused || false
|
|
||||||
|
|
||||||
currentOutput = activatedWs.output || ""
|
|
||||||
|
|
||||||
updateCurrentOutputWorkspaces()
|
|
||||||
|
|
||||||
// Force property change notifications
|
|
||||||
allWorkspacesChanged()
|
|
||||||
} else {
|
|
||||||
focusedWorkspaceIndex = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focusedWorkspaceId = data.id;
|
||||||
|
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
|
||||||
|
|
||||||
|
if (focusedWorkspaceIndex >= 0) {
|
||||||
|
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx);
|
||||||
|
|
||||||
|
updateCurrentOutputWorkspaces();
|
||||||
|
workspacesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowsChanged(data) {
|
function handleWindowsChanged(data) {
|
||||||
windows = [...data.windows].sort((a, b) => a.id - b.id)
|
windows = [...data.windows].sort((a, b) => a.id - b.id);
|
||||||
updateFocusedWindow()
|
updateFocusedWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowClosed(data) {
|
function handleWindowClosed(data) {
|
||||||
windows = windows.filter(w => w.id !== data.id)
|
windows = windows.filter(w => w.id !== data.id);
|
||||||
updateFocusedWindow()
|
updateFocusedWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowFocusChanged(data) {
|
function handleWindowFocusChanged(data) {
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
focusedWindowId = data.id
|
focusedWindowId = data.id;
|
||||||
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
|
focusedWindowIndex = windows.findIndex(w => w.id === data.id);
|
||||||
} else {
|
} else {
|
||||||
focusedWindowId = ""
|
focusedWindowId = "";
|
||||||
focusedWindowIndex = -1
|
focusedWindowIndex = -1;
|
||||||
}
|
}
|
||||||
updateFocusedWindow()
|
updateFocusedWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowOpenedOrChanged(data) {
|
function handleWindowOpenedOrChanged(data) {
|
||||||
if (!data.window) return;
|
if (!data.window)
|
||||||
|
return;
|
||||||
|
|
||||||
const window = data.window;
|
const window = data.window;
|
||||||
const existingIndex = windows.findIndex(w => w.id === window.id);
|
const existingIndex = windows.findIndex(w => w.id === window.id);
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
// Update existing window - create new array to trigger property change
|
|
||||||
let updatedWindows = [...windows];
|
let updatedWindows = [...windows];
|
||||||
updatedWindows[existingIndex] = window;
|
updatedWindows[existingIndex] = window;
|
||||||
windows = updatedWindows.sort((a, b) => a.id - b.id);
|
windows = updatedWindows.sort((a, b) => a.id - b.id);
|
||||||
} else {
|
} else {
|
||||||
// Add new window
|
|
||||||
windows = [...windows, window].sort((a, b) => a.id - b.id);
|
windows = [...windows, window].sort((a, b) => a.id - b.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update focused window if this window is focused
|
|
||||||
if (window.is_focused) {
|
if (window.is_focused) {
|
||||||
focusedWindowId = window.id;
|
focusedWindowId = window.id;
|
||||||
focusedWindowIndex = windows.findIndex(w => w.id === window.id);
|
focusedWindowIndex = windows.findIndex(w => w.id === window.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFocusedWindow();
|
updateFocusedWindow();
|
||||||
|
|
||||||
// Emit signal for other services to listen to
|
|
||||||
windowOpenedOrChanged(window);
|
windowOpenedOrChanged(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOverviewChanged(data) {
|
function handleOverviewChanged(data) {
|
||||||
inOverview = data.is_open
|
inOverview = data.is_open;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCurrentOutputWorkspaces() {
|
function updateCurrentOutputWorkspaces() {
|
||||||
if (!currentOutput) {
|
if (!currentOutput) {
|
||||||
currentOutputWorkspaces = allWorkspaces
|
currentOutputWorkspaces = allWorkspaces;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter workspaces for current output
|
var outputWs = allWorkspaces.filter(w => w.output === currentOutput);
|
||||||
var outputWs = allWorkspaces.filter(w => w.output === currentOutput)
|
currentOutputWorkspaces = outputWs;
|
||||||
currentOutputWorkspaces = outputWs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFocusedWindow() {
|
function updateFocusedWindow() {
|
||||||
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
||||||
var focusedWin = windows[focusedWindowIndex]
|
var focusedWin = windows[focusedWindowIndex];
|
||||||
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
|
focusedWindowTitle = focusedWin.title || "(Unnamed window)";
|
||||||
} else {
|
} else {
|
||||||
focusedWindowTitle = "(No active window)"
|
focusedWindowTitle = "(No active window)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API functions
|
function send(request) {
|
||||||
function switchToWorkspace(workspaceId) {
|
if (!niriAvailable || !requestSocket.connected)
|
||||||
if (!niriAvailable) return false
|
return false;
|
||||||
|
requestSocket.write(JSON.stringify(request) + "\n");
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()])
|
return true;
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToWorkspaceByIndex(index) {
|
function switchToWorkspace(workspaceIndex) {
|
||||||
if (!niriAvailable || index < 0 || index >= allWorkspaces.length) return false
|
return send({
|
||||||
|
Action: {
|
||||||
var workspace = allWorkspaces[index]
|
FocusWorkspace: {
|
||||||
return switchToWorkspace(workspace.id)
|
reference: {
|
||||||
|
Index: workspaceIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToWorkspaceByNumber(number, output) {
|
|
||||||
if (!niriAvailable) return false
|
|
||||||
|
|
||||||
var targetOutput = output || currentOutput
|
|
||||||
if (!targetOutput) {
|
|
||||||
console.warn("NiriService: No output specified for workspace switching")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get workspaces for the target output, sorted by idx
|
|
||||||
var outputWorkspaces = allWorkspaces.filter(w => w.output === targetOutput).sort((a, b) => a.idx - b.idx)
|
|
||||||
|
|
||||||
// Use sequential index (number is 1-based, array is 0-based)
|
|
||||||
if (number >= 1 && number <= outputWorkspaces.length) {
|
|
||||||
var workspace = outputWorkspaces[number - 1]
|
|
||||||
return switchToWorkspace(workspace.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn("NiriService: No workspace", number, "found on output", targetOutput)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentOutputWorkspaceNumbers() {
|
function getCurrentOutputWorkspaceNumbers() {
|
||||||
return currentOutputWorkspaces.map(w => w.idx + 1) // niri uses 0-based, UI shows 1-based
|
return currentOutputWorkspaces.map(w => w.idx + 1); // niri uses 0-based, UI shows 1-based
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentWorkspaceNumber() {
|
function getCurrentWorkspaceNumber() {
|
||||||
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
|
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
|
||||||
return allWorkspaces[focusedWorkspaceIndex].idx + 1
|
return allWorkspaces[focusedWorkspaceIndex].idx + 1;
|
||||||
}
|
}
|
||||||
return 1
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusWindow(windowId) {
|
function focusWindow(windowId) {
|
||||||
if (!niriAvailable) return false
|
return send({
|
||||||
|
Action: {
|
||||||
console.log("NiriService: Focusing window with command:", ["niri", "msg", "action", "focus-window", "--id", windowId.toString()])
|
FocusWindow: {
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", windowId.toString()])
|
id: windowId
|
||||||
return true
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeWindow(windowId) {
|
function closeWindow(windowId) {
|
||||||
if (!niriAvailable) return false
|
return send({
|
||||||
|
Action: {
|
||||||
console.log("NiriService: Closing window with command:", ["niri", "msg", "action", "close-window", "--id", windowId.toString()])
|
CloseWindow: {
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", windowId.toString()])
|
id: windowId
|
||||||
return true
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function quit() {
|
||||||
|
return send({
|
||||||
|
Action: {
|
||||||
|
Quit: {
|
||||||
|
skip_confirmation: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getWindowsByAppId(appId) {
|
function getWindowsByAppId(appId) {
|
||||||
if (!appId) return []
|
if (!appId)
|
||||||
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase())
|
return [];
|
||||||
|
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRunningAppIds() {
|
function getRunningAppIds() {
|
||||||
var appIds = new Set()
|
var appIds = new Set();
|
||||||
windows.forEach(w => {
|
windows.forEach(w => {
|
||||||
if (w.app_id) {
|
if (w.app_id) {
|
||||||
appIds.add(w.app_id.toLowerCase())
|
appIds.add(w.app_id.toLowerCase());
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return Array.from(appIds)
|
return Array.from(appIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ Rectangle {
|
|||||||
visible: root.enableFuzzySearch
|
visible: root.enableFuzzySearch
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
DankListView {
|
||||||
id: listView
|
id: listView
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
206
Widgets/DankFlickable.qml
Normal file
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
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
id: gridView
|
id: gridView
|
||||||
|
|
||||||
property int currentIndex: 0
|
// Kinetic scrolling momentum properties
|
||||||
property int columns: 4
|
property real momentumVelocity: 0
|
||||||
property bool adaptiveColumns: false
|
property bool isMomentumActive: false
|
||||||
property int minCellWidth: 120
|
property real friction: 0.95
|
||||||
property int maxCellWidth: 160
|
property real minMomentumVelocity: 50
|
||||||
property int cellPadding: 8
|
property real maxMomentumVelocity: 2500
|
||||||
property real iconSizeRatio: 0.6
|
|
||||||
property int maxIconSize: 56
|
|
||||||
property int minIconSize: 32
|
|
||||||
property bool hoverUpdatesSelection: true
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
|
|
||||||
property int baseCellHeight: baseCellWidth + 20
|
|
||||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
|
||||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
|
||||||
|
|
||||||
signal keyboardNavigationReset()
|
|
||||||
signal itemClicked(int index, var modelData)
|
|
||||||
signal itemHovered(int index)
|
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
|
||||||
|
|
||||||
function ensureVisible(index) {
|
|
||||||
if (index < 0 || index >= gridView.count)
|
|
||||||
return ;
|
|
||||||
|
|
||||||
var itemY = Math.floor(index / gridView.actualColumns) * gridView.cellHeight;
|
|
||||||
var itemBottom = itemY + gridView.cellHeight;
|
|
||||||
if (itemY < gridView.contentY)
|
|
||||||
gridView.contentY = itemY;
|
|
||||||
else if (itemBottom > gridView.contentY + gridView.height)
|
|
||||||
gridView.contentY = itemBottom - gridView.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex);
|
|
||||||
|
|
||||||
}
|
|
||||||
clip: true
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
cellWidth: baseCellWidth
|
|
||||||
cellHeight: baseCellHeight
|
|
||||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
|
||||||
rightMargin: leftMargin
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
flickDeceleration: 1500
|
||||||
maximumFlickVelocity: 2000
|
maximumFlickVelocity: 2000
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
boundsMovement: Flickable.FollowBoundsBehavior
|
||||||
pressDelay: 0
|
pressDelay: 0
|
||||||
flickableDirection: Flickable.VerticalFlick
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
// Performance optimizations
|
|
||||||
cacheBuffer: Math.min(height * 2, 1000)
|
|
||||||
reuseItems: true
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
WheelHandler {
|
||||||
id: wheelHandler
|
id: wheelHandler
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
|
|
||||||
// Tunable parameters for responsive scrolling
|
// Tunable parameters for responsive scrolling
|
||||||
property real mouseWheelSpeed: 20 // Higher = faster mouse wheel
|
property real mouseWheelSpeed: 20
|
||||||
property real touchpadSpeed: 1.8 // Touchpad sensitivity
|
// Higher = faster mouse wheel
|
||||||
|
property real touchpadSpeed: 1.8
|
||||||
|
// Touchpad sensitivity
|
||||||
property real momentumRetention: 0.92
|
property real momentumRetention: 0.92
|
||||||
property real lastWheelTime: 0
|
property real lastWheelTime: 0
|
||||||
property real momentum: 0
|
property real momentum: 0
|
||||||
|
property var velocitySamples: []
|
||||||
|
|
||||||
|
function startMomentum() {
|
||||||
|
isMomentumActive = true;
|
||||||
|
momentumTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||||
onWheel: (event) => {
|
onWheel: (event) => {
|
||||||
let currentTime = Date.now()
|
let currentTime = Date.now();
|
||||||
let timeDelta = currentTime - lastWheelTime
|
let timeDelta = currentTime - lastWheelTime;
|
||||||
lastWheelTime = currentTime
|
lastWheelTime = currentTime;
|
||||||
|
|
||||||
// Calculate scroll delta based on input type
|
// Detect mouse wheel vs touchpad
|
||||||
let delta = 0
|
const deltaY = event.angleDelta.y;
|
||||||
if (event.pixelDelta.y !== 0) {
|
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||||
// Touchpad with pixel precision
|
|
||||||
delta = event.pixelDelta.y * touchpadSpeed
|
if (isMouseWheel) {
|
||||||
|
// Fixed scrolling for mouse wheel - 2 cells per click
|
||||||
|
momentumTimer.stop();
|
||||||
|
isMomentumActive = false;
|
||||||
|
velocitySamples = [];
|
||||||
|
momentum = 0;
|
||||||
|
|
||||||
|
const lines = Math.floor(Math.abs(deltaY) / 120);
|
||||||
|
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.15; // 0.15 cells per wheel click
|
||||||
|
let newY = contentY + scrollAmount;
|
||||||
|
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||||
|
|
||||||
|
if (flicking)
|
||||||
|
cancelFlick();
|
||||||
|
|
||||||
|
contentY = newY;
|
||||||
} else {
|
} else {
|
||||||
// Mouse wheel - larger steps for faster scrolling
|
// Touchpad - existing smooth kinetic scrolling
|
||||||
delta = event.angleDelta.y / 120 * cellHeight * 2 // 2 cells per wheel step
|
// Stop any existing momentum
|
||||||
|
momentumTimer.stop();
|
||||||
|
isMomentumActive = false;
|
||||||
|
|
||||||
|
// Calculate scroll delta based on input type
|
||||||
|
let delta = 0;
|
||||||
|
if (event.pixelDelta.y !== 0)
|
||||||
|
// Touchpad with pixel precision
|
||||||
|
delta = event.pixelDelta.y * touchpadSpeed;
|
||||||
|
else
|
||||||
|
// Fallback for touchpad without pixel delta
|
||||||
|
delta = event.angleDelta.y / 120 * cellHeight * 1.2;
|
||||||
|
|
||||||
|
// Track velocity for momentum
|
||||||
|
velocitySamples.push({
|
||||||
|
"delta": delta,
|
||||||
|
"time": currentTime
|
||||||
|
});
|
||||||
|
velocitySamples = velocitySamples.filter((s) => {
|
||||||
|
return currentTime - s.time < 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate momentum velocity from samples
|
||||||
|
if (velocitySamples.length > 1) {
|
||||||
|
let totalDelta = velocitySamples.reduce((sum, s) => {
|
||||||
|
return sum + s.delta;
|
||||||
|
}, 0);
|
||||||
|
let timeSpan = currentTime - velocitySamples[0].time;
|
||||||
|
if (timeSpan > 0)
|
||||||
|
momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply momentum for touchpad (smooth continuous scrolling)
|
||||||
|
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||||
|
momentum = momentum * momentumRetention + delta * 0.15;
|
||||||
|
delta += momentum;
|
||||||
|
} else {
|
||||||
|
momentum = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply scrolling with proper bounds checking
|
||||||
|
let newY = contentY - delta;
|
||||||
|
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||||
|
|
||||||
|
// Cancel any conflicting flicks and apply new position
|
||||||
|
if (flicking)
|
||||||
|
cancelFlick();
|
||||||
|
|
||||||
|
contentY = newY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply momentum for touchpad (smooth continuous scrolling)
|
event.accepted = true;
|
||||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
}
|
||||||
momentum = momentum * momentumRetention + delta * 0.15
|
onActiveChanged: {
|
||||||
delta += momentum
|
if (!active && Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||||
} else {
|
startMomentum();
|
||||||
momentum = 0
|
} else if (!active) {
|
||||||
|
velocitySamples = [];
|
||||||
|
momentumVelocity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply scrolling with proper bounds checking
|
|
||||||
let newY = contentY - delta
|
|
||||||
newY = Math.max(0, Math.min(
|
|
||||||
contentHeight - height, newY))
|
|
||||||
|
|
||||||
// Cancel any conflicting flicks and apply new position
|
|
||||||
if (flicking) {
|
|
||||||
cancelFlick()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
// Physics-based momentum timer for kinetic scrolling
|
||||||
policy: ScrollBar.AsNeeded
|
Timer {
|
||||||
}
|
id: momentumTimer
|
||||||
|
|
||||||
ScrollBar.horizontal: ScrollBar {
|
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: gridView.cellWidth - cellPadding
|
|
||||||
height: gridView.cellHeight - cellPadding
|
|
||||||
radius: Theme.cornerRadiusLarge
|
|
||||||
color: currentIndex === index ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
|
||||||
border.color: currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
|
|
||||||
border.width: currentIndex === index ? 2 : 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property int iconSize: Math.min(maxIconSize, Math.max(minIconSize, gridView.cellWidth * iconSizeRatio))
|
|
||||||
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !iconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadiusLarge
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
|
||||||
font.pixelSize: Math.min(28, parent.width * 0.5)
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
interval: 16 // ~60 FPS
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
// Apply velocity to position
|
||||||
|
let newY = contentY - momentumVelocity * 0.016;
|
||||||
|
let maxY = Math.max(0, contentHeight - height);
|
||||||
|
|
||||||
|
// Stop momentum at boundaries instead of bouncing
|
||||||
|
if (newY < 0) {
|
||||||
|
contentY = 0;
|
||||||
|
stop();
|
||||||
|
isMomentumActive = false;
|
||||||
|
momentumVelocity = 0;
|
||||||
|
return;
|
||||||
|
} else if (newY > maxY) {
|
||||||
|
contentY = maxY;
|
||||||
|
stop();
|
||||||
|
isMomentumActive = false;
|
||||||
|
momentumVelocity = 0;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
contentY = newY;
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
width: gridView.cellWidth - 12
|
// Apply friction
|
||||||
text: model.name || ""
|
momentumVelocity *= friction;
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
// Stop if velocity too low
|
||||||
font.weight: Font.Medium
|
if (Math.abs(momentumVelocity) < 5) {
|
||||||
elide: Text.ElideRight
|
stop();
|
||||||
horizontalAlignment: Text.AlignHCenter
|
isMomentumActive = false;
|
||||||
maximumLineCount: 2
|
momentumVelocity = 0;
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: {
|
|
||||||
if (hoverUpdatesSelection && !keyboardNavigationActive)
|
|
||||||
currentIndex = index;
|
|
||||||
|
|
||||||
itemHovered(index);
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
keyboardNavigationReset();
|
|
||||||
}
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
itemClicked(index, model);
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
var globalPos = mapToGlobal(mouse.x, mouse.y);
|
|
||||||
itemRightClicked(index, model, globalPos.x, globalPos.y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// Smooth return to bounds animation
|
||||||
|
NumberAnimation {
|
||||||
|
id: returnToBoundsAnimation
|
||||||
|
|
||||||
|
target: gridView
|
||||||
|
property: "contentY"
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,219 +1,212 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: listView
|
id: listView
|
||||||
|
|
||||||
property int itemHeight: 72
|
property real mouseWheelSpeed: 12
|
||||||
property int iconSize: 56
|
|
||||||
property bool showDescription: true
|
|
||||||
property int itemSpacing: Theme.spacingS
|
|
||||||
property bool hoverUpdatesSelection: true
|
|
||||||
property bool keyboardNavigationActive: false
|
|
||||||
|
|
||||||
signal keyboardNavigationReset()
|
// Simple position preservation
|
||||||
signal itemClicked(int index, var modelData)
|
property real savedY: 0
|
||||||
signal itemHovered(int index)
|
property bool justChanged: false
|
||||||
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
|
property bool isUserScrolling: false
|
||||||
|
|
||||||
function ensureVisible(index) {
|
// Kinetic scrolling momentum properties
|
||||||
if (index < 0 || index >= count)
|
property real momentumVelocity: 0
|
||||||
return ;
|
property bool isMomentumActive: false
|
||||||
|
property real friction: 0.95
|
||||||
|
property real minMomentumVelocity: 50
|
||||||
|
property real maxMomentumVelocity: 2500
|
||||||
|
|
||||||
var itemY = index * (itemHeight + itemSpacing);
|
|
||||||
var itemBottom = itemY + itemHeight;
|
|
||||||
if (itemY < contentY)
|
|
||||||
contentY = itemY;
|
|
||||||
else if (itemBottom > contentY + height)
|
|
||||||
contentY = itemBottom - height;
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
|
||||||
if (keyboardNavigationActive)
|
|
||||||
ensureVisible(currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
anchors.margins: itemSpacing
|
|
||||||
spacing: itemSpacing
|
|
||||||
focus: true
|
|
||||||
interactive: true
|
|
||||||
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
flickDeceleration: 1500
|
||||||
maximumFlickVelocity: 2000
|
maximumFlickVelocity: 2000
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
boundsMovement: Flickable.FollowBoundsBehavior
|
boundsMovement: Flickable.FollowBoundsBehavior
|
||||||
pressDelay: 0
|
pressDelay: 0
|
||||||
flickableDirection: Flickable.VerticalFlick
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
|
onMovementStarted: isUserScrolling = true
|
||||||
|
onMovementEnded: isUserScrolling = false
|
||||||
|
|
||||||
// Performance optimizations
|
onContentYChanged: {
|
||||||
cacheBuffer: Math.min(height * 2, 1000)
|
if (!justChanged && isUserScrolling) {
|
||||||
reuseItems: true
|
savedY = contentY;
|
||||||
|
}
|
||||||
|
justChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
// Restore position when model changes
|
||||||
|
onModelChanged: {
|
||||||
|
justChanged = true;
|
||||||
|
contentY = savedY;
|
||||||
|
}
|
||||||
|
|
||||||
WheelHandler {
|
WheelHandler {
|
||||||
id: wheelHandler
|
id: wheelHandler
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
|
|
||||||
// Tunable parameters for responsive scrolling
|
// Tunable parameters for responsive scrolling
|
||||||
property real mouseWheelSpeed: 20 // Higher = faster mouse wheel
|
property real touchpadSpeed: 1.8 // Touchpad sensitivity
|
||||||
property real touchpadSpeed: 1.8 // Touchpad sensitivity
|
|
||||||
property real momentumRetention: 0.92
|
property real momentumRetention: 0.92
|
||||||
property real lastWheelTime: 0
|
property real lastWheelTime: 0
|
||||||
property real momentum: 0
|
property real momentum: 0
|
||||||
|
property var velocitySamples: []
|
||||||
|
|
||||||
|
function startMomentum() {
|
||||||
|
isMomentumActive = true;
|
||||||
|
momentumTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||||
|
|
||||||
onWheel: (event) => {
|
onWheel: (event) => {
|
||||||
let currentTime = Date.now()
|
isUserScrolling = true; // Mark as user interaction
|
||||||
let timeDelta = currentTime - lastWheelTime
|
|
||||||
lastWheelTime = currentTime
|
|
||||||
|
|
||||||
// Calculate scroll delta based on input type
|
let currentTime = Date.now();
|
||||||
let delta = 0
|
let timeDelta = currentTime - lastWheelTime;
|
||||||
if (event.pixelDelta.y !== 0) {
|
lastWheelTime = currentTime;
|
||||||
// Touchpad with pixel precision
|
|
||||||
delta = event.pixelDelta.y * touchpadSpeed
|
// Detect mouse wheel vs touchpad, seems like assuming based on the increments is the only way in QT
|
||||||
|
const deltaY = event.angleDelta.y;
|
||||||
|
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||||
|
|
||||||
|
if (isMouseWheel) {
|
||||||
|
momentumTimer.stop();
|
||||||
|
isMomentumActive = false;
|
||||||
|
velocitySamples = [];
|
||||||
|
momentum = 0;
|
||||||
|
|
||||||
|
const lines = Math.floor(Math.abs(deltaY) / 120);
|
||||||
|
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
|
||||||
|
let newY = listView.contentY + scrollAmount;
|
||||||
|
newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY));
|
||||||
|
|
||||||
|
if (listView.flicking)
|
||||||
|
listView.cancelFlick();
|
||||||
|
|
||||||
|
listView.contentY = newY;
|
||||||
|
savedY = newY;
|
||||||
} else {
|
} else {
|
||||||
// Mouse wheel - larger steps for faster scrolling
|
momentumTimer.stop();
|
||||||
delta = event.angleDelta.y / 120 * itemHeight * 2.5 // 2.5 items per wheel step
|
isMomentumActive = false;
|
||||||
|
|
||||||
|
// Calculate scroll delta based on input type
|
||||||
|
let delta = 0;
|
||||||
|
if (event.pixelDelta.y !== 0) {
|
||||||
|
// Touchpad with pixel precision
|
||||||
|
delta = event.pixelDelta.y * touchpadSpeed;
|
||||||
|
} else {
|
||||||
|
// Fallback for touchpad without pixel delta
|
||||||
|
delta = event.angleDelta.y / 8 * touchpadSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track velocity for momentum
|
||||||
|
velocitySamples.push({
|
||||||
|
"delta": delta,
|
||||||
|
"time": currentTime
|
||||||
|
});
|
||||||
|
velocitySamples = velocitySamples.filter((s) => {
|
||||||
|
return currentTime - s.time < 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate momentum velocity from samples
|
||||||
|
if (velocitySamples.length > 1) {
|
||||||
|
let totalDelta = velocitySamples.reduce((sum, s) => {
|
||||||
|
return sum + s.delta;
|
||||||
|
}, 0);
|
||||||
|
let timeSpan = currentTime - velocitySamples[0].time;
|
||||||
|
if (timeSpan > 0)
|
||||||
|
momentumVelocity = Math.max(-maxMomentumVelocity,
|
||||||
|
Math.min(maxMomentumVelocity,
|
||||||
|
totalDelta / timeSpan * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply momentum for touchpad (smooth continuous scrolling)
|
||||||
|
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||||
|
momentum = momentum * momentumRetention + delta * 0.15;
|
||||||
|
delta += momentum;
|
||||||
|
} else {
|
||||||
|
momentum = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply scrolling with proper bounds checking
|
||||||
|
let newY = listView.contentY - delta;
|
||||||
|
newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY));
|
||||||
|
|
||||||
|
// Cancel any conflicting flicks and apply new position
|
||||||
|
if (listView.flicking)
|
||||||
|
listView.cancelFlick();
|
||||||
|
|
||||||
|
listView.contentY = newY;
|
||||||
|
savedY = newY; // Update saved position
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply momentum for touchpad (smooth continuous scrolling)
|
event.accepted = true;
|
||||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
}
|
||||||
momentum = momentum * momentumRetention + delta * 0.15
|
|
||||||
delta += momentum
|
onActiveChanged: {
|
||||||
} else {
|
if (!active) {
|
||||||
momentum = 0
|
isUserScrolling = false;
|
||||||
|
|
||||||
|
// Start momentum if applicable (touchpad only)
|
||||||
|
if (Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||||
|
startMomentum();
|
||||||
|
} else {
|
||||||
|
velocitySamples = [];
|
||||||
|
momentumVelocity = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply scrolling with proper bounds checking
|
|
||||||
let newY = listView.contentY - delta
|
|
||||||
newY = Math.max(0, Math.min(
|
|
||||||
listView.contentHeight - listView.height, newY))
|
|
||||||
|
|
||||||
// Cancel any conflicting flicks and apply new position
|
|
||||||
if (listView.flicking) {
|
|
||||||
listView.cancelFlick()
|
|
||||||
}
|
|
||||||
|
|
||||||
listView.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
// Physics-based momentum timer for kinetic scrolling (touchpad only)
|
||||||
policy: ScrollBar.AlwaysOn
|
Timer {
|
||||||
}
|
id: momentumTimer
|
||||||
|
interval: 16 // ~60 FPS
|
||||||
ScrollBar.horizontal: ScrollBar {
|
repeat: true
|
||||||
policy: ScrollBar.AlwaysOff
|
|
||||||
}
|
onTriggered: {
|
||||||
|
// Apply velocity to position
|
||||||
delegate: Rectangle {
|
let newY = contentY - momentumVelocity * 0.016;
|
||||||
width: ListView.view.width
|
let maxY = Math.max(0, contentHeight - height);
|
||||||
height: itemHeight
|
|
||||||
radius: Theme.cornerRadiusLarge
|
// Stop momentum at boundaries instead of bouncing
|
||||||
color: ListView.isCurrentItem ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
if (newY < 0) {
|
||||||
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
|
contentY = 0;
|
||||||
border.width: ListView.isCurrentItem ? 2 : 1
|
savedY = 0;
|
||||||
|
stop();
|
||||||
Row {
|
isMomentumActive = false;
|
||||||
anchors.fill: parent
|
momentumVelocity = 0;
|
||||||
anchors.margins: Theme.spacingM
|
return;
|
||||||
spacing: Theme.spacingL
|
} else if (newY > maxY) {
|
||||||
|
contentY = maxY;
|
||||||
Item {
|
savedY = maxY;
|
||||||
width: iconSize
|
stop();
|
||||||
height: iconSize
|
isMomentumActive = false;
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
momentumVelocity = 0;
|
||||||
|
return;
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
|
|
||||||
smooth: true
|
|
||||||
asynchronous: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: !iconImg.visible
|
|
||||||
color: Theme.surfaceLight
|
|
||||||
radius: Theme.cornerRadiusLarge
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.primarySelected
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
|
||||||
font.pixelSize: iconSize * 0.4
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Bold
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
contentY = newY;
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
savedY = newY; // Keep updating saved position during momentum
|
||||||
width: parent.width - iconSize - Theme.spacingL
|
|
||||||
spacing: Theme.spacingXS
|
// Apply friction
|
||||||
|
momentumVelocity *= friction;
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
// Stop if velocity too low
|
||||||
text: model.name || ""
|
if (Math.abs(momentumVelocity) < 5) {
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
stop();
|
||||||
color: Theme.surfaceText
|
isMomentumActive = false;
|
||||||
font.weight: Font.Medium
|
momentumVelocity = 0;
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: model.comment || "Application"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
visible: showDescription && model.comment && model.comment.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
z: 10
|
|
||||||
onEntered: {
|
|
||||||
if (hoverUpdatesSelection && !keyboardNavigationActive)
|
|
||||||
currentIndex = index;
|
|
||||||
|
|
||||||
itemHovered(index);
|
|
||||||
}
|
|
||||||
onPositionChanged: {
|
|
||||||
keyboardNavigationReset();
|
|
||||||
}
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
itemClicked(index, model);
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
var globalPos = mapToGlobal(mouse.x, mouse.y);
|
|
||||||
itemRightClicked(index, model, globalPos.x, globalPos.y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// Smooth return to bounds animation
|
||||||
|
NumberAnimation {
|
||||||
|
id: returnToBoundsAnimation
|
||||||
|
target: listView
|
||||||
|
property: "contentY"
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -250,7 +250,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingS
|
anchors.margins: Theme.spacingS
|
||||||
|
|
||||||
ListView {
|
DankListView {
|
||||||
id: searchResultsList
|
id: searchResultsList
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Column {
|
|||||||
signal compactModeChanged(string widgetId, bool enabled)
|
signal compactModeChanged(string widgetId, bool enabled)
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
height: implicitHeight
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -258,6 +259,7 @@ Column {
|
|||||||
drag.axis: Drag.YAxis
|
drag.axis: Drag.YAxis
|
||||||
drag.minimumY: -delegateItem.height
|
drag.minimumY: -delegateItem.height
|
||||||
drag.maximumY: itemsList.height
|
drag.maximumY: itemsList.height
|
||||||
|
preventStealing: true
|
||||||
onPressed: {
|
onPressed: {
|
||||||
delegateItem.z = 2;
|
delegateItem.z = 2;
|
||||||
delegateItem.originalY = delegateItem.y;
|
delegateItem.originalY = delegateItem.y;
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ Popup {
|
|||||||
height: parent.height - 120 // Leave space for header and description
|
height: parent.height - 120 // Leave space for header and description
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
ListView {
|
DankListView {
|
||||||
id: widgetList
|
id: widgetList
|
||||||
|
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|||||||
@@ -56,36 +56,35 @@ update_theme_settings() {
|
|||||||
|
|
||||||
update_gtk_css() {
|
update_gtk_css() {
|
||||||
local config_dir="$1"
|
local config_dir="$1"
|
||||||
local import_line="@import url(\"$config_dir/gtk-4.0/dank-colors.css\");"
|
local is_light="$2"
|
||||||
|
local shell_dir="$3"
|
||||||
|
|
||||||
echo "Updating GTK CSS imports..."
|
echo "Updating GTK CSS..."
|
||||||
|
|
||||||
# Update GTK-4.0
|
# GTK-3.0: Copy the appropriate template file
|
||||||
|
local gtk3_css="$config_dir/gtk-3.0/gtk.css"
|
||||||
|
if [ "$is_light" = "true" ]; then
|
||||||
|
echo "Copying light GTK-3.0 template..."
|
||||||
|
cp "$shell_dir/templates/gtk3-colloid-light.css" "$gtk3_css"
|
||||||
|
else
|
||||||
|
echo "Copying dark GTK-3.0 template..."
|
||||||
|
cp "$shell_dir/templates/gtk3-colloid-dark.css" "$gtk3_css"
|
||||||
|
fi
|
||||||
|
echo "Updated GTK-3.0 CSS"
|
||||||
|
|
||||||
|
# GTK-4.0: Use simplified import
|
||||||
|
local gtk4_import="@import url(\"dank-colors.css\");"
|
||||||
local gtk4_css="$config_dir/gtk-4.0/gtk.css"
|
local gtk4_css="$config_dir/gtk-4.0/gtk.css"
|
||||||
if [ -f "$gtk4_css" ]; then
|
if [ -f "$gtk4_css" ]; then
|
||||||
# Remove existing import if present
|
# Remove existing import if present
|
||||||
sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk4_css"
|
sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk4_css"
|
||||||
# Add import at the top
|
# Add import at the top
|
||||||
sed -i "1i\\$import_line" "$gtk4_css"
|
sed -i "1i\\$gtk4_import" "$gtk4_css"
|
||||||
else
|
else
|
||||||
# Create new gtk.css with import
|
# Create new gtk.css with import
|
||||||
echo "$import_line" > "$gtk4_css"
|
echo "$gtk4_import" > "$gtk4_css"
|
||||||
fi
|
fi
|
||||||
echo "Updated GTK-4.0 CSS import"
|
echo "Updated GTK-4.0 CSS import"
|
||||||
|
|
||||||
# Update GTK-3.0 with its own path
|
|
||||||
local gtk3_import="@import url(\"$config_dir/gtk-3.0/dank-colors.css\");"
|
|
||||||
local gtk3_css="$config_dir/gtk-3.0/gtk.css"
|
|
||||||
if [ -f "$gtk3_css" ]; then
|
|
||||||
# Remove existing import if present
|
|
||||||
sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk3_css"
|
|
||||||
# Add import at the top
|
|
||||||
sed -i "1i\\$gtk3_import" "$gtk3_css"
|
|
||||||
else
|
|
||||||
# Create new gtk.css with import
|
|
||||||
echo "$gtk3_import" > "$gtk3_css"
|
|
||||||
fi
|
|
||||||
echo "Updated GTK-3.0 CSS import"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update_qt_config() {
|
update_qt_config() {
|
||||||
@@ -240,7 +239,7 @@ update_theme_settings "$color_scheme" "$ICON_THEME"
|
|||||||
|
|
||||||
# Update GTK CSS imports if GTK theming is enabled
|
# Update GTK CSS imports if GTK theming is enabled
|
||||||
if [ "$GTK_THEMING" = "true" ]; then
|
if [ "$GTK_THEMING" = "true" ]; then
|
||||||
update_gtk_css "$CONFIG_DIR"
|
update_gtk_css "$CONFIG_DIR" "$IS_LIGHT" "$SHELL_DIR"
|
||||||
echo "GTK theming updated"
|
echo "GTK theming updated"
|
||||||
else
|
else
|
||||||
echo "GTK theming disabled - skipping GTK CSS updates"
|
echo "GTK theming disabled - skipping GTK CSS updates"
|
||||||
|
|||||||
17
shell.qml
17
shell.qml
@@ -1,3 +1,4 @@
|
|||||||
|
//@ pragma UseQApplication
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -21,8 +22,7 @@ import qs.Services
|
|||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
WallpaperBackground {
|
WallpaperBackground {}
|
||||||
}
|
|
||||||
|
|
||||||
Lock {
|
Lock {
|
||||||
id: lock
|
id: lock
|
||||||
@@ -36,7 +36,6 @@ ShellRoot {
|
|||||||
delegate: TopBar {
|
delegate: TopBar {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
@@ -47,17 +46,12 @@ ShellRoot {
|
|||||||
contextMenu: dockContextMenu
|
contextMenu: dockContextMenu
|
||||||
windowsMenu: dockWindowsMenu
|
windowsMenu: dockWindowsMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CentcomPopout {
|
CentcomPopout {
|
||||||
id: centcomPopout
|
id: centcomPopout
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemTrayContextMenu {
|
|
||||||
id: systemTrayContextMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
DockContextMenu {
|
DockContextMenu {
|
||||||
id: dockContextMenu
|
id: dockContextMenu
|
||||||
}
|
}
|
||||||
@@ -76,7 +70,6 @@ ShellRoot {
|
|||||||
delegate: NotificationPopupManager {
|
delegate: NotificationPopupManager {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ControlCenterPopout {
|
ControlCenterPopout {
|
||||||
@@ -141,7 +134,6 @@ ShellRoot {
|
|||||||
ProcessListModal {
|
ProcessListModal {
|
||||||
id: processListModal
|
id: processListModal
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
@@ -177,7 +169,6 @@ ShellRoot {
|
|||||||
delegate: Toast {
|
delegate: Toast {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
@@ -186,7 +177,6 @@ ShellRoot {
|
|||||||
delegate: VolumePopup {
|
delegate: VolumePopup {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
@@ -195,7 +185,6 @@ ShellRoot {
|
|||||||
delegate: MicMutePopup {
|
delegate: MicMutePopup {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
@@ -204,7 +193,5 @@ ShellRoot {
|
|||||||
delegate: BrightnessPopup {
|
delegate: BrightnessPopup {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
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