1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/quickshell/Modules/AppDrawer/AppDrawerPopout.qml
2025-12-01 09:31:57 -05:00

835 lines
35 KiB
QML

import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankPopout {
id: appDrawerPopout
layerNamespace: "dms:app-launcher"
function show() {
open();
}
popupWidth: 520
popupHeight: 600
triggerWidth: 40
positioning: ""
onBackgroundClicked: {
if (contextMenu.visible) {
contextMenu.close();
}
close();
}
onOpened: {
appLauncher.searchQuery = "";
appLauncher.selectedIndex = 0;
appLauncher.setCategory(I18n.tr("All"));
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();
}
contextMenu.parent = contentLoader.item;
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.appLauncherViewMode
gridColumns: SettingsData.appLauncherGridColumns
onAppLaunched: appDrawerPopout.close()
onViewModeSelected: function (mode) {
SettingsData.set("appLauncherViewMode", mode);
}
}
content: Component {
Rectangle {
id: launcherPanel
property alias searchField: searchField
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
antialiasing: true
smooth: true
// Multi-layer border effect
Repeater {
model: [
{
"margin": -3,
"color": Qt.rgba(0, 0, 0, 0.05),
"z": -3
},
{
"margin": -2,
"color": Qt.rgba(0, 0, 0, 0.08),
"z": -2
},
{
"margin": 0,
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
"z": -1
}
]
Rectangle {
anchors.fill: parent
anchors.margins: modelData.margin
color: "transparent"
radius: parent.radius + Math.abs(modelData.margin)
border.color: modelData.color
border.width: 0
z: modelData.z
}
}
Item {
id: keyHandler
anchors.fill: parent
focus: true
readonly property var keyMappings: {
const mappings = {};
mappings[Qt.Key_Escape] = () => appDrawerPopout.close();
mappings[Qt.Key_Down] = () => appLauncher.selectNext();
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious();
mappings[Qt.Key_Return] = () => appLauncher.launchSelected();
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected();
mappings[Qt.Key_Tab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : appLauncher.selectNext();
mappings[Qt.Key_Backtab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : appLauncher.selectPrevious();
if (appLauncher.viewMode === "grid") {
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow();
mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow();
}
return mappings;
}
Keys.onPressed: function (event) {
if (keyMappings[event.key]) {
keyMappings[event.key]();
event.accepted = true;
return;
}
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext();
event.accepted = true;
return;
}
if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious();
event.accepted = true;
return;
}
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext();
event.accepted = true;
return;
}
if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious();
event.accepted = true;
return;
}
if (appLauncher.viewMode === "grid") {
if (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNextInRow();
event.accepted = true;
return;
}
if (event.key === Qt.Key_H && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPreviousInRow();
event.accepted = true;
return;
}
}
}
Column {
width: parent.width - Theme.spacingS * 2
height: parent.height - Theme.spacingS * 2
x: Theme.spacingS
y: Theme.spacingS
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Applications")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
DankTextField {
id: searchField
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
height: 52
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [keyHandler]
onTextEdited: {
appLauncher.searchQuery = text;
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.close();
event.accepted = true;
return;
}
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key);
const hasText = text.length > 0;
if (isEnterKey && hasText) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
appLauncher.launchSelected();
} else if (appLauncher.model.count > 0) {
appLauncher.launchApp(appLauncher.model.get(0));
}
event.accepted = true;
return;
}
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab];
const isNavigationKey = navigationKeys.includes(event.key);
const isEmptyEnter = isEnterKey && !hasText;
event.accepted = !(isNavigationKey || isEmptyEnter);
}
Connections {
function onShouldBeVisibleChanged() {
if (!appDrawerPopout.shouldBeVisible) {
searchField.focus = false;
}
}
target: appDrawerPopout
}
}
Item {
width: parent.width - Theme.spacingS * 2
height: 40
anchors.horizontalCenter: parent.horizontalCenter
visible: searchField.text.length === 0
Rectangle {
width: 180
height: 40
radius: Theme.cornerRadius
color: "transparent"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
DankDropdown {
anchors.fill: parent
text: ""
dropdownWidth: 180
currentValue: appLauncher.selectedCategory
options: appLauncher.categories
optionIcons: appLauncher.categoryIcons
onValueChanged: function (value) {
appLauncher.setCategory(value);
}
}
}
Row {
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 36
circular: false
iconName: "view_list"
iconSize: 20
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("list");
}
}
DankActionButton {
buttonSize: 36
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("grid");
}
}
}
}
Rectangle {
width: searchField.width
x: searchField.x
height: {
let usedHeight = 40 + Theme.spacingS;
usedHeight += 52 + Theme.spacingS;
usedHeight += (searchField.text.length === 0 ? 40 : 0);
return parent.height - usedHeight;
}
radius: Theme.cornerRadius
color: "transparent"
DankListView {
id: appList
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 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.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData);
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData);
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
delegate: AppLauncherListDelegate {
listView: appList
itemHeight: appList.itemHeight
iconSize: appList.iconSize
showDescription: appList.showDescription
hoverUpdatesSelection: appList.hoverUpdatesSelection
keyboardNavigationActive: appList.keyboardNavigationActive
isCurrentItem: ListView.isCurrentItem
mouseAreaLeftMargin: Theme.spacingS
mouseAreaRightMargin: Theme.spacingS
mouseAreaBottomMargin: Theme.spacingM
iconMargins: Theme.spacingXS
iconFallbackLeftMargin: Theme.spacingS
iconFallbackRightMargin: Theme.spacingS
iconFallbackBottomMargin: Theme.spacingM
onItemClicked: (idx, modelData) => appList.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
const panelPos = contextMenu.parent.mapFromItem(null, mouseX, mouseY);
appList.itemRightClicked(idx, modelData, panelPos.x, panelPos.y);
}
onKeyboardNavigationReset: appList.keyboardNavigationReset
}
}
DankGridView {
id: appGrid
property int currentIndex: appLauncher.selectedIndex
property int columns: appLauncher.gridColumns
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property real iconSizeRatio: 0.6
property int maxIconSize: 56
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property real baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : width / columns
property real 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 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.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData);
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData);
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false;
}
delegate: AppLauncherGridDelegate {
gridView: appGrid
cellWidth: appGrid.cellWidth
cellHeight: appGrid.cellHeight
minIconSize: appGrid.minIconSize
maxIconSize: appGrid.maxIconSize
iconSizeRatio: appGrid.iconSizeRatio
hoverUpdatesSelection: appGrid.hoverUpdatesSelection
keyboardNavigationActive: appGrid.keyboardNavigationActive
currentIndex: appGrid.currentIndex
mouseAreaLeftMargin: Theme.spacingS
mouseAreaRightMargin: Theme.spacingS
mouseAreaBottomMargin: Theme.spacingS
iconFallbackLeftMargin: Theme.spacingS
iconFallbackRightMargin: Theme.spacingS
iconFallbackBottomMargin: Theme.spacingS
iconMaterialSizeAdjustment: Theme.spacingL
onItemClicked: (idx, modelData) => appGrid.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
const panelPos = contextMenu.parent.mapFromItem(null, mouseX, mouseY);
appGrid.itemRightClicked(idx, modelData, panelPos.x, panelPos.y);
}
onKeyboardNavigationReset: appGrid.keyboardNavigationReset
}
}
}
}
}
MouseArea {
anchors.fill: parent
visible: contextMenu.visible
z: 998
onClicked: contextMenu.hide()
}
}
}
Popup {
id: contextMenu
property var currentApp: null
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
readonly property string appId: desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
function show(x, y, app) {
currentApp = app;
let finalX = x + 4;
let finalY = y + 4;
if (contextMenu.parent) {
const parentWidth = contextMenu.parent.width;
const parentHeight = contextMenu.parent.height;
const menuWidth = contextMenu.width;
const menuHeight = contextMenu.height;
if (finalX + menuWidth > parentWidth) {
finalX = Math.max(0, parentWidth - menuWidth);
}
if (finalY + menuHeight > parentHeight) {
finalY = Math.max(0, parentHeight - menuHeight);
}
}
contextMenu.x = finalX;
contextMenu.y = finalY;
contextMenu.open();
}
function hide() {
contextMenu.close();
}
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
background: Rectangle {
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
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: -1
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: contextMenu.isPinned ? "keep_off" : "push_pin"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: contextMenu.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.desktopEntry) {
return;
}
if (contextMenu.isPinned) {
SessionData.removePinnedApp(contextMenu.appId);
} else {
SessionData.addPinnedApp(contextMenu.appId);
}
contextMenu.hide();
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Repeater {
model: contextMenu.desktopEntry && contextMenu.desktopEntry.actions ? contextMenu.desktopEntry.actions : []
Rectangle {
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize - 2
height: Theme.iconSize - 2
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: actionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && contextMenu.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.desktopEntry, modelData);
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp);
}
}
contextMenu.hide();
}
}
}
}
Rectangle {
visible: contextMenu.desktopEntry && contextMenu.desktopEntry.actions && contextMenu.desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp)
appLauncher.launchApp(contextMenu.currentApp);
contextMenu.hide();
}
}
}
Rectangle {
visible: SessionService.nvidiaCommand
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
visible: SessionService.nvidiaCommand
width: parent.width
height: 32
radius: Theme.cornerRadius
color: nvidiaMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: nvidiaMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.desktopEntry, true);
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp);
}
}
contextMenu.hide();
}
}
}
}
}
}