1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

refactor: perf improvement stopping singletons with ref

Also switch to scale+opacity anims with custom curve
This commit is contained in:
bbedward
2025-07-23 16:54:19 -04:00
parent 2c57487046
commit 4f63d5899b
21 changed files with 677 additions and 672 deletions

View File

@@ -7,11 +7,16 @@ import Quickshell
Singleton {
id: root
readonly property int durShort: 160
readonly property int durMed: 220
readonly property int durLong: 320
readonly property int durShort: 200
readonly property int durMed: 400
readonly property int durLong: 600
readonly property int slidePx: 100
readonly property int slidePx: 80
// Material Design 3 motion curves
readonly property var emphasized: [0.05, 0, 2/15, 0.06, 1/6, 0.4, 5/24, 0.82, 0.25, 1, 1, 1]
readonly property var emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property var standard: [0.2, 0, 0, 1, 1, 1]
readonly property QtObject direction: QtObject {
readonly property int fromLeft: 0
@@ -20,30 +25,26 @@ Singleton {
}
readonly property Component slideInLeft: Transition {
NumberAnimation { properties: "x"; from: -Anims.slidePx; to: 0; duration: Anims.durMed; easing.type: Easing.OutCubic }
NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0; duration: Anims.durShort }
NumberAnimation { properties: "x"; from: -Anims.slidePx; to: 0; duration: Anims.durMed; easing.type: Easing.OutQuart }
}
readonly property Component slideOutLeft: Transition {
NumberAnimation { properties: "x"; to: -Anims.slidePx; duration: Anims.durShort; easing.type: Easing.InCubic }
NumberAnimation { properties: "opacity"; to: 0.0; duration: Anims.durShort }
NumberAnimation { properties: "x"; to: -Anims.slidePx; duration: Anims.durShort; easing.type: Easing.InQuart }
}
readonly property Component slideInRight: Transition {
NumberAnimation { properties: "x"; from: Anims.slidePx; to: 0; duration: Anims.durMed; easing.type: Easing.OutCubic }
NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0; duration: Anims.durShort }
NumberAnimation { properties: "x"; from: Anims.slidePx; to: 0; duration: Anims.durMed; easing.type: Easing.OutQuart }
}
readonly property Component slideOutRight: Transition {
NumberAnimation { properties: "x"; to: Anims.slidePx; duration: Anims.durShort; easing.type: Easing.InCubic }
NumberAnimation { properties: "opacity"; to: 0.0; duration: Anims.durShort }
NumberAnimation { properties: "x"; to: Anims.slidePx; duration: Anims.durShort; easing.type: Easing.InQuart }
}
readonly property Component fadeIn: Transition {
NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0; duration: Anims.durShort; easing.type: Easing.OutCubic }
NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0; duration: Anims.durMed; easing.type: Easing.OutQuart }
}
readonly property Component fadeOut: Transition {
NumberAnimation { properties: "opacity"; to: 0.0; duration: Anims.durShort; easing.type: Easing.InCubic }
NumberAnimation { properties: "opacity"; to: 0.0; duration: Anims.durShort; easing.type: Easing.InQuart }
}
}

9
Common/Ref.qml Normal file
View File

@@ -0,0 +1,9 @@
import Quickshell
import QtQuick
QtObject {
required property Singleton service
Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount--
}

View File

@@ -19,8 +19,6 @@ DankModal {
function show() {
processListModal.visible = true;
ProcessMonitorService.updateSystemInfo();
ProcessMonitorService.updateProcessList();
SystemMonitorService.enableDetailedMonitoring(true);
SystemMonitorService.updateSystemInfo();
UserInfoService.getUptime();
@@ -46,9 +44,10 @@ DankModal {
cornerRadius: Theme.cornerRadiusXLarge
enableShadow: true
onVisibleChanged: {
ProcessMonitorService.enableMonitoring(visible);
Ref {
service: ProcessMonitorService
}
onBackgroundClicked: hide()

View File

@@ -71,7 +71,9 @@ DankModal {
gridColumns: 4
onAppLaunched: hide()
onViewModeSelected: Prefs.setSpotlightModalViewMode(mode)
onViewModeSelected: function(mode) {
Prefs.setSpotlightModalViewMode(mode);
}
}
content: Component {
@@ -265,12 +267,16 @@ DankModal {
iconSize: 40
showDescription: true
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
// Grid view
@@ -288,12 +294,16 @@ DankModal {
maxIconSize: 48
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
}
}

View File

@@ -14,21 +14,14 @@ PanelWindow {
id: appDrawerPopout
property bool isVisible: false
property bool showCategories: false
function show() {
appDrawerPopout.isVisible = true;
searchField.enabled = true;
appLauncher.searchQuery = "";
}
function hide() {
searchField.enabled = false; // Disable before hiding to prevent Wayland warnings
appDrawerPopout.isVisible = false;
searchField.text = "";
showCategories = false;
}
function toggle() {
@@ -62,491 +55,341 @@ PanelWindow {
gridColumns: 4
onAppLaunched: appDrawerPopout.hide()
onViewModeSelected: Prefs.setAppLauncherViewMode(mode)
onViewModeSelected: function(mode) {
Prefs.setAppLauncherViewMode(mode);
}
}
// Background click to close (no visual background)
MouseArea {
anchors.fill: parent
enabled: appDrawerPopout.isVisible
onClicked: appDrawerPopout.hide()
onClicked: function(mouse) {
// Only close if click is outside the launcher panel
var localPos = mapToItem(launcherLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > launcherLoader.width ||
localPos.y < 0 || localPos.y > launcherLoader.height) {
appDrawerPopout.hide();
}
}
}
Component {
id: iconComponent
Item {
property var appData: parent.modelData || {
// Main launcher panel with asynchronous loading
Loader {
id: launcherLoader
asynchronous: true
active: appDrawerPopout.isVisible
width: 520
height: 600
x: Theme.spacingL
y: Theme.barHeight + Theme.spacingXS
// GPU-accelerated scale + opacity animation
opacity: appDrawerPopout.isVisible ? 1 : 0
scale: appDrawerPopout.isVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
id: launcherPanel
color: Theme.popupBackground()
radius: Theme.cornerRadiusXLarge
// Remove layer rendering for better performance
antialiasing: true
smooth: true
IconImage {
id: iconImg
// Material 3 elevation with multiple layers
Rectangle {
anchors.fill: parent
source: (appData && appData.icon) ? Quickshell.iconPath(appData.icon, "") : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
anchors.fill: parent
visible: !iconImg.visible
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
radius: Theme.cornerRadiusLarge
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
Text {
anchors.centerIn: parent
text: (appData && appData.name && appData.name.length > 0) ? appData.name.charAt(0).toUpperCase() : "A"
font.pixelSize: 28
color: Theme.primary
font.weight: Font.Bold
}
z: -2
}
}
}
// Main launcher panel with enhanced design
Rectangle {
id: launcherPanel
width: 520
height: 600
color: Theme.popupBackground()
radius: Theme.cornerRadiusXLarge
opacity: appDrawerPopout.isVisible ? 1 : 0
x: appDrawerPopout.isVisible ? Theme.spacingL : Theme.spacingL - Anims.slidePx
y: Theme.barHeight + Theme.spacingXS
Behavior on x {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.OutCubic
}
}
// Material 3 elevation with multiple layers
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 1
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Qt.rgba(0, 0, 0, 0.08)
border.width: 1
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
// Content with focus management
Item {
anchors.fill: parent
focus: true
Component.onCompleted: {
if (appDrawerPopout.isVisible)
forceActiveFocus();
}
// Handle keyboard shortcuts
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.hide();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious();
event.accepted = true;
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
appLauncher.launchSelected();
event.accepted = true;
} else if (event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\s]/)) {
// User started typing, focus search field and pass the character
searchField.forceActiveFocus();
searchField.text = event.text;
event.accepted = true;
}
}
Column {
Rectangle {
anchors.fill: parent
anchors.margins: Theme.spacingXL
spacing: Theme.spacingL
color: "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
radius: parent.radius
z: -1
}
// Header section
Row {
width: parent.width
height: 40
// App launcher title
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
// Content with focus management
Item {
anchors.fill: parent
focus: true
Component.onCompleted: {
if (appDrawerPopout.isVisible)
forceActiveFocus();
}
// Handle keyboard shortcuts
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.hide();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious();
event.accepted = true;
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
appLauncher.launchSelected();
event.accepted = true;
} else if (event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
// User started typing, focus search field and pass the character
searchField.forceActiveFocus();
searchField.text = event.text;
event.accepted = true;
}
Item {
width: parent.width - 200
height: 1
}
// Quick stats
Text {
anchors.verticalCenter: parent.verticalCenter
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
// Enhanced search field
DankTextField {
id: searchField
Column {
width: parent.width - Theme.spacingXL * 2
height: parent.height - Theme.spacingXL * 2
x: Theme.spacingXL
y: Theme.spacingXL
spacing: Theme.spacingL
width: parent.width
height: 52
cornerRadius: Theme.cornerRadiusLarge
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.isVisible
placeholderText: "Search applications..."
onTextEdited: {
appLauncher.searchQuery = text;
}
Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && appLauncher.model.count && text.length > 0) {
// Launch first app when typing in search field
var firstApp = appLauncher.model.get(0);
appLauncher.launchApp(firstApp);
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") || (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
// Pass navigation keys and enter (when not searching) to main handler
event.accepted = false;
}
}
// Header section
Row {
width: parent.width
height: 40
Connections {
function onVisibleChanged() {
if (appDrawerPopout.visible)
searchField.forceActiveFocus();
else
searchField.clearFocus();
}
target: appDrawerPopout
}
}
// Category filter and view mode controls
Row {
width: parent.width
height: 40
spacing: Theme.spacingM
visible: searchField.text.length === 0
// Category filter
Rectangle {
width: 200
height: 36
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
// App launcher title
Text {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "category"
size: 18
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: appLauncher.selectedCategory
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
font.weight: Font.Medium
}
text: "Applications"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
DankIcon {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
Item {
width: parent.width - 200
height: 1
}
// Quick stats
Text {
anchors.verticalCenter: parent.verticalCenter
name: showCategories ? "expand_less" : "expand_more"
size: 18
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showCategories = !showCategories
// Enhanced search field
DankTextField {
id: searchField
width: parent.width
height: 52
cornerRadius: Theme.cornerRadiusLarge
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.isVisible
placeholderText: "Search applications..."
onTextEdited: {
appLauncher.searchQuery = text;
}
Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && appLauncher.model.count && text.length > 0) {
// Launch first app when typing in search field
var firstApp = appLauncher.model.get(0);
appLauncher.launchApp(firstApp);
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") || (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
// Pass navigation keys and enter (when not searching) to main handler
event.accepted = false;
}
}
Connections {
function onVisibleChanged() {
if (appDrawerPopout.visible)
searchField.forceActiveFocus();
else
searchField.clearFocus();
}
target: appDrawerPopout
}
}
Item {
width: parent.width - 300
height: 1
}
// View mode toggle
// Category filter and view mode controls
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 40
spacing: Theme.spacingM
visible: searchField.text.length === 0
// List view button
DankActionButton {
buttonSize: 36
circular: false
iconName: "view_list"
iconSize: 20
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("list");
}
}
// Grid view button
DankActionButton {
buttonSize: 36
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("grid");
}
}
}
}
// App grid/list container
Rectangle {
width: parent.width
height: {
// Calculate more precise remaining height
let usedHeight = 40 + Theme.spacingL;
// Header
usedHeight += 52 + Theme.spacingL;
// Search container
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0);
// Category/controls when visible
return parent.height - usedHeight;
}
color: "transparent"
// List view
DankListView {
id: appList
anchors.fill: parent
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 72
iconSize: 56
showDescription: true
hoverUpdatesSelection: false
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
}
// Grid view
DankGridView {
id: appGrid
anchors.fill: parent
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
}
}
// Category dropdown overlay - now positioned absolutely
Rectangle {
id: categoryDropdown
width: 200
height: Math.min(250, categories.length * 40 + Theme.spacingM * 2)
radius: Theme.cornerRadiusLarge
color: Theme.contentBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
visible: showCategories
z: 1000
// Position it below the category button
anchors.top: parent.top
anchors.topMargin: 140 + (searchField.text.length === 0 ? 0 : -40)
anchors.left: parent.left
// Drop shadow
Rectangle {
anchors.fill: parent
anchors.margins: -4
color: "transparent"
radius: parent.radius + 4
z: -1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
shadowBlur: 0.25 // radius/32
shadowColor: Qt.rgba(0, 0, 0, 0.2)
shadowOpacity: 0.2
}
}
ScrollView {
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
// Make mouse wheel scrolling more responsive
property real wheelStepSize: 60
model: appLauncher.categories
spacing: 4
MouseArea {
// Category filter using DankDropdown
Item {
width: 200
height: 36
DankDropdown {
anchors.fill: parent
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
onWheel: (wheel) => {
var delta = wheel.angleDelta.y;
var steps = delta / 120; // Standard wheel step
parent.contentY -= steps * parent.wheelStepSize;
// Ensure we stay within bounds
if (parent.contentY < 0)
parent.contentY = 0;
else if (parent.contentY > parent.contentHeight - parent.height)
parent.contentY = Math.max(0, parent.contentHeight - parent.height);
text: ""
currentValue: appLauncher.selectedCategory
options: appLauncher.categories
optionIcons: appLauncher.categoryIcons
onValueChanged: function(value) {
appLauncher.setCategory(value);
}
}
delegate: Rectangle {
width: ListView.view.width
height: 36
radius: Theme.cornerRadiusSmall
color: catArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: appLauncher.selectedCategory === modelData ? Theme.primary : Theme.surfaceText
font.weight: appLauncher.selectedCategory === modelData ? Font.Medium : Font.Normal
}
MouseArea {
id: catArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
appLauncher.setCategory(modelData);
showCategories = false;
}
}
}
}
Item {
width: parent.width - 300
height: 1
}
// View mode toggle
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
// List view button
DankActionButton {
buttonSize: 36
circular: false
iconName: "view_list"
iconSize: 20
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("list");
}
}
// Grid view button
DankActionButton {
buttonSize: 36
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
hoverColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("grid");
}
}
}
}
// App grid/list container
Rectangle {
width: parent.width
height: {
// Calculate more precise remaining height
let usedHeight = 40 + Theme.spacingL;
// Header
usedHeight += 52 + Theme.spacingL;
// Search container
usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0);
// Category/controls when visible
return parent.height - usedHeight;
}
color: "transparent"
// List view
DankListView {
id: appList
anchors.fill: parent
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 72
iconSize: 56
showDescription: true
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
// Grid view
DankGridView {
id: appGrid
anchors.fill: parent
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
appLauncher.selectedIndex = index;
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.OutCubic
}
}
}
}
}

View File

@@ -17,6 +17,7 @@ Item {
property int gridColumns: 4
property bool debounceSearch: true
property int debounceInterval: 50
property bool keyboardNavigationActive: false
// Categories (computed from AppSearchService)
property var categories: {
@@ -28,6 +29,9 @@ Item {
return cat !== "All";
}));
}
// Category icons (computed from AppSearchService)
property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
// Recent apps helper
property var recentApps: Prefs.recentlyUsedApps.map(recentApp => {
@@ -129,6 +133,7 @@ Item {
// Keyboard navigation functions
function selectNext() {
if (filteredModel.count > 0) {
keyboardNavigationActive = true;
if (viewMode === "grid") {
var newIndex = Math.min(selectedIndex + gridColumns, filteredModel.count - 1);
selectedIndex = newIndex;
@@ -140,6 +145,7 @@ Item {
function selectPrevious() {
if (filteredModel.count > 0) {
keyboardNavigationActive = true;
if (viewMode === "grid") {
var newIndex = Math.max(selectedIndex - gridColumns, 0);
selectedIndex = newIndex;
@@ -151,12 +157,14 @@ Item {
function selectNextInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true;
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
}
}
function selectPreviousInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true;
selectedIndex = Math.max(selectedIndex - 1, 0);
}
}

View File

@@ -87,13 +87,23 @@ PanelWindow {
border.width: 1
layer.enabled: true
opacity: calendarVisible ? 1 : 0
scale: calendarVisible ? 1 : 0.9
x: (Screen.width - targetWidth) / 2
y: Theme.barHeight + 4
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.OutCubic
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
@@ -241,8 +251,13 @@ PanelWindow {
anchors.fill: parent
z: -1
enabled: calendarVisible
onClicked: {
calendarVisible = false;
onClicked: function(mouse) {
// Only close if click is outside the main container
var localPos = mapToItem(mainContainer, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > mainContainer.width ||
localPos.y < 0 || localPos.y > mainContainer.height) {
calendarVisible = false;
}
}
}

View File

@@ -317,8 +317,8 @@ Rectangle {
x: 0
y: 0
width: mediaPlayerWidget.width
height: mediaPlayerWidget.height
width: mediaPlayer.width
height: mediaPlayer.height
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true

View File

@@ -15,6 +15,10 @@ Rectangle {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Ref {
service: WeatherService
}
// Placeholder when no weather - centered in entire widget
Column {

View File

@@ -47,25 +47,46 @@ PanelWindow {
bottom: true
}
Rectangle {
Loader {
id: contentLoader
asynchronous: true
active: controlCenterVisible
readonly property real targetWidth: Math.min(600, Screen.width - Theme.spacingL * 2)
width: targetWidth
height: root.powerOptionsExpanded ? 570 : 500
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
x: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
// GPU-accelerated scale + opacity animation
opacity: controlCenterVisible ? 1 : 0
property real normalX: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
x: controlCenterVisible ? normalX : normalX + Anims.slidePx
Behavior on x {
scale: controlCenterVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.OutCubic
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
// Remove layer rendering for better performance
antialiasing: true
smooth: true
ColumnLayout {
anchors.fill: parent
@@ -647,30 +668,27 @@ PanelWindow {
}
// Power menu height animation
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration // Faster for height changes
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.OutCubic
// Power menu height animation
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration // Faster for height changes
easing.type: Theme.standardEasing
}
}
}
}
// Click outside to close
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
controlCenterVisible = false;
onClicked: function(mouse) {
// Only close if click is outside the content loader
var localPos = mapToItem(contentLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentLoader.width ||
localPos.y < 0 || localPos.y > contentLoader.height) {
controlCenterVisible = false;
}
}
}

View File

@@ -119,7 +119,7 @@ Column {
model: NetworkService.wifiAvailable && NetworkService.wifiEnabled ? sortedWifiNetworks : []
Rectangle {
width: parent.width
width: spanningNetworksColumn.width
height: 38
radius: Theme.cornerRadiusSmall
color: networkArea2.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"

View File

@@ -23,8 +23,6 @@ PanelWindow {
function show() {
isVisible = true;
ProcessMonitorService.updateSystemInfo();
ProcessMonitorService.updateProcessList();
}
function toggle() {
@@ -35,9 +33,11 @@ PanelWindow {
}
visible: isVisible
onIsVisibleChanged: {
ProcessMonitorService.enableMonitoring(isVisible);
Ref {
service: ProcessMonitorService
}
implicitWidth: 600
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
@@ -54,84 +54,86 @@ PanelWindow {
MouseArea {
anchors.fill: parent
onClicked: processListPopout.hide()
onClicked: function(mouse) {
// Only close if click is outside the content loader
var localPos = mapToItem(contentLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentLoader.width ||
localPos.y < 0 || localPos.y > contentLoader.height) {
processListPopout.hide();
}
}
}
Rectangle {
id: dropdownContent
Loader {
id: contentLoader
asynchronous: true
active: processListPopout.isVisible
readonly property real targetWidth: Math.min(600, Screen.width - Theme.spacingL * 2)
readonly property real targetHeight: Math.min(600, Screen.height - Theme.barHeight - Theme.spacingS * 2)
width: targetWidth
height: targetHeight
y: Theme.barHeight + Theme.spacingXS
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
clip: true
x: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
// GPU-accelerated scale + opacity animation
opacity: processListPopout.isVisible ? 1 : 0
layer.enabled: true
property real normalX: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
x: processListPopout.isVisible ? normalX : normalX + Anims.slidePx
Behavior on x {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.OutCubic
}
}
MouseArea {
anchors.fill: parent
onClicked: {
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
SystemOverview {
Layout.fillWidth: true
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
ProcessListView {
Layout.fillWidth: true
Layout.fillHeight: true
contextMenu: processContextMenuWindow
processContextMenuWindow: processContextMenuWindow
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: processListPopout.isVisible ? 0.15 : 0
}
scale: processListPopout.isVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.OutCubic
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
id: dropdownContent
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
clip: true
// Remove layer rendering for better performance
antialiasing: true
smooth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
SystemOverview {
Layout.fillWidth: true
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
ProcessListView {
Layout.fillWidth: true
Layout.fillHeight: true
contextMenu: processContextMenuWindow
}
}
}
}
ProcessContextMenu {
id: processContextMenuWindow
}
}

View File

@@ -48,33 +48,58 @@ PanelWindow {
// Click outside to dismiss overlay
MouseArea {
anchors.fill: parent
onClicked: {
batteryPopupVisible = false;
onClicked: function(mouse) {
// Only close if click is outside the content loader
var localPos = mapToItem(contentLoader, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentLoader.width ||
localPos.y < 0 || localPos.y > contentLoader.height) {
batteryPopupVisible = false;
}
}
}
Rectangle {
Loader {
id: contentLoader
asynchronous: true
active: batteryPopupVisible
readonly property real targetWidth: Math.min(380, Screen.width - Theme.spacingL * 2)
readonly property real targetHeight: Math.min(450, Screen.height - Theme.barHeight - Theme.spacingS * 2)
width: targetWidth
height: targetHeight
property real normalX: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
x: batteryPopupVisible ? normalX : normalX + Anims.slidePx
x: Math.max(Theme.spacingL, Screen.width - targetWidth - Theme.spacingL)
y: Theme.barHeight + Theme.spacingS
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
// GPU-accelerated scale + opacity animation
opacity: batteryPopupVisible ? 1 : 0
// Prevent click-through to background
MouseArea {
// Consume the click to prevent it from reaching the background
anchors.fill: parent
onClicked: {
scale: batteryPopupVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
// Remove layer rendering for better performance
antialiasing: true
smooth: true
ScrollView {
anchors.fill: parent
@@ -461,20 +486,8 @@ PanelWindow {
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.OutCubic
}
}
Behavior on x {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.OutCubic
}
}
}

View File

@@ -162,7 +162,7 @@ PanelWindow {
Media {
anchors.verticalCenter: parent.verticalCenter
anchors.right: clockWidget.left
anchors.right: clock.left
anchors.rightMargin: Theme.spacingS
visible: Prefs.showMusic && MprisController.activePlayer
onClicked: {

View File

@@ -13,6 +13,10 @@ Rectangle {
height: 30
radius: Theme.cornerRadius
color: weatherArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Ref {
service: WeatherService
}
Row {
id: weatherRow

View File

@@ -116,6 +116,25 @@ Singleton {
return Array.from(mappedCategories)
}
// Category icon mappings
property var categoryIcons: ({
"All": "apps",
"Recents": "history",
"Media": "music_video",
"Development": "code",
"Games": "sports_esports",
"Graphics": "photo_library",
"Internet": "web",
"Office": "content_paste",
"Settings": "settings",
"System": "host",
"Utilities": "build"
})
function getCategoryIcon(category) {
return categoryIcons[category] || "folder"
}
function getAllCategories() {
var categories = new Set(["All"])

View File

@@ -6,14 +6,13 @@ import Quickshell
import Quickshell.Io
Singleton {
// console.log("ProcessMonitorService: Updated - CPU:", root.totalCpuUsage.toFixed(1) + "%", "Memory:", memoryPercent.toFixed(1) + "%", "History length:", root.cpuHistory.length)
id: root
property int refCount
property var processes: []
property bool isUpdating: false
property int processUpdateInterval: 3000
property bool monitoringEnabled: false
property int totalMemoryKB: 0
property int usedMemoryKB: 0
property int totalSwapKB: 0
@@ -43,47 +42,8 @@ Singleton {
property bool sortDescending: true
property int maxProcesses: 20
function updateSystemInfo() {
if (!systemInfoProcess.running && root.monitoringEnabled)
systemInfoProcess.running = true;
}
function enableMonitoring(enabled) {
console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled");
root.monitoringEnabled = enabled;
if (enabled) {
root.cpuHistory = [];
root.memoryHistory = [];
root.networkHistory = ({
"rx": [],
"tx": []
});
root.diskHistory = ({
"read": [],
"write": []
});
updateSystemInfo();
updateProcessList();
updateNetworkStats();
updateDiskStats();
}
}
function updateNetworkStats() {
if (!networkStatsProcess.running && root.monitoringEnabled)
networkStatsProcess.running = true;
}
function updateDiskStats() {
if (!diskStatsProcess.running && root.monitoringEnabled)
diskStatsProcess.running = true;
}
function updateProcessList() {
if (!root.isUpdating && root.monitoringEnabled) {
if (!root.isUpdating) {
root.isUpdating = true;
let sortOption = "";
switch (root.sortBy) {
@@ -315,10 +275,17 @@ Singleton {
}
Component.onCompleted: {
console.log("ProcessMonitorService: Starting initialization...");
updateProcessList();
console.log("ProcessMonitorService: Initialization complete");
Timer {
running: root.refCount > 0
interval: root.processUpdateInterval
repeat: true
triggeredOnStart: true
onTriggered: {
systemInfoProcess.running = true;
updateProcessList();
networkStatsProcess.running = true;
diskStatsProcess.running = true;
}
}
Process {
@@ -441,20 +408,4 @@ Singleton {
}
}
Timer {
id: processTimer
interval: root.processUpdateInterval
running: root.monitoringEnabled
repeat: true
onTriggered: {
if (root.monitoringEnabled) {
updateSystemInfo();
updateProcessList();
updateNetworkStats();
updateDiskStats();
}
}
}
}

View File

@@ -9,6 +9,8 @@ import qs.Common
Singleton {
id: root
property int refCount: 0
property var weather: ({
available: false,
loading: true,
@@ -95,7 +97,27 @@ Singleton {
return url
}
function addRef() {
refCount++;
console.log("WeatherService: addRef, refCount now:", refCount);
if (refCount === 1 && !weather.available) {
// Start fetching when first consumer appears
fetchWeather();
}
}
function removeRef() {
refCount = Math.max(0, refCount - 1);
console.log("WeatherService: removeRef, refCount now:", refCount);
}
function fetchWeather() {
// Only fetch if someone is consuming the data
if (root.refCount === 0) {
console.log("WeatherService: Skipping fetch - no consumers");
return;
}
if (weatherFetcher.running) {
console.log("Weather fetch already in progress, skipping")
return
@@ -217,7 +239,7 @@ Singleton {
Timer {
id: updateTimer
interval: root.updateInterval
running: true
running: root.refCount > 0
repeat: true
triggeredOnStart: true
onTriggered: {

View File

@@ -12,6 +12,7 @@ Rectangle {
property string description: ""
property string currentValue: ""
property var options: []
property var optionIcons: [] // Array of icon names corresponding to options
signal valueChanged(string value)
@@ -72,13 +73,33 @@ Rectangle {
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
Text {
text: root.currentValue
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
width: parent.width - 24
elide: Text.ElideRight
DankIcon {
name: {
var currentIndex = root.options.indexOf(root.currentValue);
return root.optionIcons.length > currentIndex && currentIndex >= 0 ? root.optionIcons[currentIndex] : "";
}
size: 18
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
visible: {
var currentIndex = root.options.indexOf(root.currentValue);
return root.optionIcons.length > currentIndex && currentIndex >= 0 && root.optionIcons[currentIndex] !== "";
}
}
Text {
text: root.currentValue
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - (visible ? 24 + Theme.spacingS : 24) - (parent.children[0].visible ? 18 + Theme.spacingS : 0)
elide: Text.ElideRight
}
}
DankIcon {
@@ -170,14 +191,27 @@ Rectangle {
radius: Theme.cornerRadiusSmall
color: optionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Text {
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: root.currentValue === modelData ? Theme.primary : Theme.surfaceText
font.weight: root.currentValue === modelData ? Font.Medium : Font.Normal
spacing: Theme.spacingS
DankIcon {
name: root.optionIcons.length > index ? root.optionIcons[index] : ""
size: 18
color: root.currentValue === modelData ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
visible: root.optionIcons.length > index && root.optionIcons[index] !== ""
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: root.currentValue === modelData ? Theme.primary : Theme.surfaceText
font.weight: root.currentValue === modelData ? Font.Medium : Font.Normal
}
}
MouseArea {

View File

@@ -19,10 +19,33 @@ ScrollView {
property int minIconSize: 32
property real wheelStepSize: 60
property bool hoverUpdatesSelection: true
property bool keyboardNavigationActive: false
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
// Ensure the current item is visible
function ensureVisible(index) {
if (index < 0 || index >= grid.count) return;
var itemY = Math.floor(index / grid.actualColumns) * grid.cellHeight;
var itemBottom = itemY + grid.cellHeight;
if (itemY < grid.contentY) {
grid.contentY = itemY;
} else if (itemBottom > grid.contentY + grid.height) {
grid.contentY = itemBottom - grid.height;
}
}
onCurrentIndexChanged: {
if (keyboardNavigationActive) {
ensureVisible(currentIndex);
}
}
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
@@ -134,11 +157,15 @@ ScrollView {
cursorShape: Qt.PointingHandCursor
z: 10
onEntered: {
if (hoverUpdatesSelection)
if (hoverUpdatesSelection && !keyboardNavigationActive) {
currentIndex = index;
}
itemHovered(index);
}
onPositionChanged: {
// Signal parent to reset keyboard navigation flag when mouse moves
keyboardNavigationReset();
}
onClicked: {
itemClicked(index, model);
}

View File

@@ -15,10 +15,32 @@ ScrollView {
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: true
property bool keyboardNavigationActive: false
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
// Ensure the current item is visible
function ensureVisible(index) {
if (index < 0 || index >= list.count) return;
var itemY = index * (itemHeight + itemSpacing);
var itemBottom = itemY + itemHeight;
if (itemY < list.contentY) {
list.contentY = itemY;
} else if (itemBottom > list.contentY + list.height) {
list.contentY = itemBottom - list.height;
}
}
onCurrentIndexChanged: {
if (keyboardNavigationActive) {
ensureVisible(currentIndex);
}
}
clip: true
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
@@ -134,11 +156,15 @@ ScrollView {
cursorShape: Qt.PointingHandCursor
z: 10
onEntered: {
if (hoverUpdatesSelection)
if (hoverUpdatesSelection && !keyboardNavigationActive) {
listView.currentIndex = index;
}
itemHovered(index);
}
onPositionChanged: {
// Signal parent to reset keyboard navigation flag when mouse moves
keyboardNavigationReset();
}
onClicked: {
itemClicked(index, model);
}