1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 21:45:38 -05:00

refactor: app drawer de-dupe

This commit is contained in:
bbedward
2025-07-23 15:36:29 -04:00
parent c01da89311
commit 94b10159a9
13 changed files with 505 additions and 604 deletions

View File

@@ -1,46 +0,0 @@
import QtQuick
import qs.Common
Item {
id: root
// attach to target
required property Item target
property int direction: Anims.direction.fadeOnly
// call these
function show() { _apply(true) }
function hide() { _apply(false) }
function _apply(showing) {
const off = Anims.slidePx
let fromX = 0
let toX = 0
switch(direction) {
case Anims.direction.fromLeft: fromX = -off; toX = 0; break
case Anims.direction.fromRight: fromX = off; toX = 0; break
default: fromX = 0; toX = 0;
}
if (showing) {
target.x = fromX
target.opacity = 0
target.visible = true
animX.from = fromX; animX.to = toX
animO.from = 0; animO.to = 1
} else {
animX.from = target.x; animX.to = (direction === Anims.direction.fromLeft ? -off :
direction === Anims.direction.fromRight ? off : 0)
animO.from = target.opacity; animO.to = 0
}
seq.restart()
}
SequentialAnimation {
id: seq
ParallelAnimation {
NumberAnimation { id: animX; target: root.target; property: "x"; duration: Anims.durMed; easing.type: Easing.OutCubic }
NumberAnimation { id: animO; target: root.target; property: "opacity"; duration: Anims.durShort }
}
ScriptAction { script: if (root.target.opacity === 0) root.target.visible = false }
}
}

View File

@@ -6,43 +6,25 @@ import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.AppDrawer
DankModal {
id: spotlightModal
property bool spotlightOpen: false
property var filteredApps: []
property int selectedIndex: 0
property int maxResults: 50
property var categories: {
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
return cat !== "Education" && cat !== "Science";
});
// Insert "Recents" after "All"
var result = ["All", "Recents"];
return result.concat(allCategories.filter((cat) => {
return cat !== "All";
}));
}
property string selectedCategory: "All"
property string viewMode: Prefs.spotlightModalViewMode // "list" or "grid"
property int gridColumns: 4
function show() {
console.log("SpotlightModal: show() called");
spotlightOpen = true;
console.log("SpotlightModal: spotlightOpen set to", spotlightOpen);
searchDebounceTimer.stop(); // Stop any pending search
updateFilteredApps(); // Immediate update when showing
appLauncher.searchQuery = "";
}
function hide() {
spotlightOpen = false;
searchDebounceTimer.stop(); // Stop any pending search
searchQuery = "";
selectedIndex = 0;
selectedCategory = "All";
updateFilteredApps();
appLauncher.searchQuery = "";
appLauncher.selectedIndex = 0;
appLauncher.setCategory("All");
}
function toggle() {
@@ -52,149 +34,8 @@ DankModal {
show();
}
property string searchQuery: ""
function updateFilteredApps() {
filteredApps = [];
selectedIndex = 0;
var apps = [];
if (searchQuery.length === 0) {
// Show apps from category
if (selectedCategory === "All") {
// For "All" category, show all available apps
apps = AppSearchService.applications || [];
} else if (selectedCategory === "Recents") {
// For "Recents" category, get recent apps from Prefs and filter out non-existent ones
var recentApps = Prefs.getRecentApps();
apps = recentApps.map((recentApp) => {
return AppSearchService.getAppByExec(recentApp.exec);
}).filter((app) => {
return app !== null && !app.noDisplay;
});
} else {
// For specific categories, limit results
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
apps = categoryApps.slice(0, maxResults);
}
} else {
// Search with category filter
if (selectedCategory === "All") {
// For "All" category, search all apps without limit
apps = AppSearchService.searchApplications(searchQuery);
} else if (selectedCategory === "Recents") {
// For "Recents" category, search within recent apps
var recentApps = Prefs.getRecentApps();
var recentDesktopEntries = recentApps.map((recentApp) => {
return AppSearchService.getAppByExec(recentApp.exec);
}).filter((app) => {
return app !== null && !app.noDisplay;
});
if (recentDesktopEntries.length > 0) {
var allSearchResults = AppSearchService.searchApplications(searchQuery);
var recentNames = new Set(recentDesktopEntries.map((app) => {
return app.name;
}));
// Filter search results to only include recent apps
apps = allSearchResults.filter((searchApp) => {
return recentNames.has(searchApp.name);
});
} else {
apps = [];
}
} else {
// For specific categories, filter search results by category
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
if (categoryApps.length > 0) {
var allSearchResults = AppSearchService.searchApplications(searchQuery);
var categoryNames = new Set(categoryApps.map((app) => {
return app.name;
}));
// Filter search results to only include apps from the selected category
apps = allSearchResults.filter((searchApp) => {
return categoryNames.has(searchApp.name);
}).slice(0, maxResults);
} else {
apps = [];
}
}
}
// Convert to our format - batch operations for better performance
filteredApps = apps.map((app) => {
return ({
"name": app.name,
"exec": app.execString || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
});
});
// Clear and repopulate model efficiently
filteredModel.clear();
filteredApps.forEach((app) => {
return filteredModel.append(app);
});
}
function launchApp(app) {
Prefs.addRecentApp(app);
if (app.desktopEntry) {
app.desktopEntry.execute();
} else {
var cleanExec = app.exec.replace(/%[fFuU]/g, "").trim();
console.log("Spotlight: Launching app directly:", cleanExec);
Quickshell.execDetached(["sh", "-c", cleanExec]);
}
hide();
}
function selectNext() {
if (filteredModel.count > 0) {
if (viewMode === "grid") {
// Grid navigation: move DOWN by one row (gridColumns positions)
var columnsCount = gridColumns;
var newIndex = Math.min(selectedIndex + columnsCount, filteredModel.count - 1);
selectedIndex = newIndex;
} else {
// List navigation: next item
selectedIndex = (selectedIndex + 1) % filteredModel.count;
}
}
}
function selectPrevious() {
if (filteredModel.count > 0) {
if (viewMode === "grid") {
// Grid navigation: move UP by one row (gridColumns positions)
var columnsCount = gridColumns;
var newIndex = Math.max(selectedIndex - columnsCount, 0);
selectedIndex = newIndex;
} else {
// List navigation: previous item
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredModel.count - 1;
}
}
}
function selectNextInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
// Grid navigation: move RIGHT by one position
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
}
}
function selectPreviousInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
// Grid navigation: move LEFT by one position
selectedIndex = Math.max(selectedIndex - 1, 0);
}
}
function launchSelected() {
if (filteredModel.count > 0 && selectedIndex >= 0 && selectedIndex < filteredModel.count) {
var selectedApp = filteredModel.get(selectedIndex);
launchApp(selectedApp);
}
}
// DankModal configuration
visible: spotlightOpen
@@ -220,26 +61,17 @@ DankModal {
Component.onCompleted: {
console.log("SpotlightModal: Component.onCompleted called - component loaded successfully!");
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
return cat !== "Education" && cat !== "Science";
});
// Insert "Recents" after "All"
var result = ["All", "Recents"];
categories = result.concat(allCategories.filter((cat) => {
return cat !== "All";
}));
}
// Search debouncing
Timer {
id: searchDebounceTimer
interval: 50
repeat: false
onTriggered: updateFilteredApps()
}
ListModel {
id: filteredModel
// App launcher logic
AppLauncher {
id: appLauncher
viewMode: Prefs.spotlightModalViewMode
gridColumns: 4
onAppLaunched: hide()
onViewModeSelected: Prefs.setSpotlightModalViewMode(mode)
}
content: Component {
@@ -253,19 +85,19 @@ DankModal {
hide();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
selectNext();
appLauncher.selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
selectPrevious();
appLauncher.selectPrevious();
event.accepted = true;
} else if (event.key === Qt.Key_Right && viewMode === "grid") {
selectNextInRow();
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && viewMode === "grid") {
selectPreviousInRow();
} 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) {
launchSelected();
appLauncher.launchSelected();
event.accepted = true;
} else if (event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) {
searchField.text = event.text;
@@ -280,99 +112,15 @@ DankModal {
spacing: Theme.spacingM
// Combined row for categories and view mode toggle
Column {
// Category selector
CategorySelector {
width: parent.width
spacing: Theme.spacingM
visible: categories.length > 1 || filteredModel.count > 0
// Categories organized in 2 rows: 4 + 5
Column {
width: parent.width
spacing: Theme.spacingS
// Top row: All, Development, Graphics, Internet (4 items)
Row {
property var topRowCategories: ["All", "Recents", "Development", "Graphics"]
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.topRowCategories.filter((cat) => {
return categories.includes(cat);
})
Rectangle {
height: 36
width: (parent.width - (parent.topRowCategories.length - 1) * Theme.spacingS) / parent.topRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
updateFilteredApps();
}
}
}
}
}
// Bottom row: Media, Office, Settings, System, Utilities (5 items)
Row {
property var bottomRowCategories: ["Internet", "Media", "Office", "Settings", "System"]
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.bottomRowCategories.filter((cat) => {
return categories.includes(cat);
})
Rectangle {
height: 36
width: (parent.width - (parent.bottomRowCategories.length - 1) * Theme.spacingS) / parent.bottomRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
updateFilteredApps();
}
}
}
}
}
}
categories: appLauncher.categories
selectedCategory: appLauncher.selectedCategory
compact: false
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0
onCategorySelected: appLauncher.setCategory(category)
}
// Search field with view toggle buttons
@@ -398,10 +146,9 @@ DankModal {
font.pixelSize: Theme.fontSizeLarge
enabled: spotlightOpen
placeholderText: "Search applications..."
text: searchQuery
text: appLauncher.searchQuery
onTextEdited: {
searchQuery = text;
searchDebounceTimer.restart();
appLauncher.searchQuery = text;
}
Connections {
@@ -418,16 +165,16 @@ DankModal {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && searchQuery.length > 0) {
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && appLauncher.searchQuery.length > 0) {
// Launch first app when typing in search field
if (filteredApps.length > 0) {
launchApp(filteredApps[0]);
if (appLauncher.model.count > 0) {
appLauncher.launchApp(appLauncher.model.get(0));
}
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up ||
(event.key === Qt.Key_Left && viewMode === "grid") ||
(event.key === Qt.Key_Right && viewMode === "grid") ||
((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && searchQuery.length === 0)) {
(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) && appLauncher.searchQuery.length === 0)) {
// Pass navigation keys and enter (when not searching) to main handler
event.accepted = false;
}
@@ -437,7 +184,7 @@ DankModal {
// View mode toggle buttons next to search bar
Row {
spacing: Theme.spacingXS
visible: filteredModel.count > 0
visible: appLauncher.model.count > 0
anchors.verticalCenter: parent.verticalCenter
// List view button
@@ -445,15 +192,15 @@ DankModal {
width: 36
height: 36
radius: Theme.cornerRadiusLarge
color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : listViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
border.color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
color: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : listViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
border.color: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
border.width: 1
DankIcon {
anchors.centerIn: parent
name: "view_list"
size: 18
color: viewMode === "list" ? Theme.primary : Theme.surfaceText
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
}
MouseArea {
@@ -463,8 +210,7 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
viewMode = "list";
Prefs.setSpotlightModalViewMode("list");
appLauncher.setViewMode("list");
}
}
}
@@ -474,15 +220,15 @@ DankModal {
width: 36
height: 36
radius: Theme.cornerRadiusLarge
color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : gridViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
border.color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
color: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : gridViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
border.color: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
border.width: 1
DankIcon {
anchors.centerIn: parent
name: "grid_view"
size: 18
color: viewMode === "grid" ? Theme.primary : Theme.surfaceText
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
}
MouseArea {
@@ -492,8 +238,7 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
viewMode = "grid";
Prefs.setSpotlightModalViewMode("grid");
appLauncher.setViewMode("grid");
}
}
}
@@ -513,18 +258,18 @@ DankModal {
id: resultsList
anchors.fill: parent
visible: viewMode === "list"
model: filteredModel
currentIndex: selectedIndex
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 60
iconSize: 40
showDescription: true
hoverUpdatesSelection: false
onItemClicked: function(index, modelData) {
launchApp(modelData);
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
selectedIndex = index;
appLauncher.selectedIndex = index;
}
}
@@ -533,21 +278,21 @@ DankModal {
id: resultsGrid
anchors.fill: parent
visible: viewMode === "grid"
model: filteredModel
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
minCellWidth: 120
maxCellWidth: 160
iconSizeRatio: 0.55
maxIconSize: 48
currentIndex: selectedIndex
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
onItemClicked: function(index, modelData) {
launchApp(modelData);
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
selectedIndex = index;
appLauncher.selectedIndex = index;
}
}
}

View File

@@ -8,153 +8,25 @@ import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.AppDrawer
PanelWindow {
// For recents, use the recent apps from Prefs and filter out non-existent ones
id: appDrawerPopout
property bool isVisible: false
// App management
property var categories: AppSearchService.getAllCategories()
property string selectedCategory: "All"
property var recentApps: Prefs.recentlyUsedApps.map((recentApp) => {
var app = AppSearchService.getAppByExec(recentApp.exec);
return app && !app.noDisplay ? app : null;
}).filter((app) => {
return app !== null;
})
property var pinnedApps: ["firefox", "code", "terminal", "file-manager"]
property bool showCategories: false
property string viewMode: Prefs.appLauncherViewMode // "list" or "grid"
property int selectedIndex: 0
function updateFilteredModel() {
filteredModel.clear();
selectedIndex = 0;
var apps = [];
var searchQuery = searchField ? searchField.text : "";
// Get apps based on category and search
if (searchQuery.length > 0) {
// Search across all apps or category
var baseApps = selectedCategory === "All" ? AppSearchService.applications : selectedCategory === "Recents" ? recentApps.map((recentApp) => {
return AppSearchService.getAppByExec(recentApp.exec);
}).filter((app) => {
return app !== null && !app.noDisplay;
}) : AppSearchService.getAppsInCategory(selectedCategory);
if (baseApps && baseApps.length > 0) {
var searchResults = AppSearchService.searchApplications(searchQuery);
apps = searchResults.filter((app) => {
return baseApps.includes(app);
});
}
} else {
// Just category filter
if (selectedCategory === "Recents")
apps = recentApps.map((recentApp) => {
return AppSearchService.getAppByExec(recentApp.exec);
}).filter((app) => {
return app !== null && !app.noDisplay;
});
else
apps = AppSearchService.getAppsInCategory(selectedCategory) || [];
}
// Add to model with null checks
if (apps && apps.length > 0)
apps.forEach((app) => {
if (app)
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
});
});
}
function selectNext() {
if (filteredModel.count > 0) {
if (viewMode === "grid") {
// Grid navigation: move by columns
var columnsCount = appGrid.columns || 4;
var newIndex = Math.min(selectedIndex + columnsCount, filteredModel.count - 1);
console.log("Grid navigation DOWN: from", selectedIndex, "to", newIndex, "columns:", columnsCount);
selectedIndex = newIndex;
} else {
// List navigation: next item
selectedIndex = (selectedIndex + 1) % filteredModel.count;
}
}
}
function selectPrevious() {
if (filteredModel.count > 0) {
if (viewMode === "grid") {
// Grid navigation: move by columns
var columnsCount = appGrid.columns || 4;
var newIndex = Math.max(selectedIndex - columnsCount, 0);
console.log("Grid navigation UP: from", selectedIndex, "to", newIndex, "columns:", columnsCount);
selectedIndex = newIndex;
} else {
// List navigation: previous item
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredModel.count - 1;
}
}
}
function selectNextInRow() {
if (filteredModel.count > 0 && viewMode === "grid")
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
}
function selectPreviousInRow() {
if (filteredModel.count > 0 && viewMode === "grid")
selectedIndex = Math.max(selectedIndex - 1, 0);
}
function launchSelected() {
if (filteredModel.count > 0 && selectedIndex >= 0 && selectedIndex < filteredModel.count) {
var selectedApp = filteredModel.get(selectedIndex);
if (selectedApp.desktopEntry) {
Prefs.addRecentApp(selectedApp.desktopEntry);
selectedApp.desktopEntry.execute();
} else {
appDrawerPopout.launchApp(selectedApp.exec);
}
appDrawerPopout.hide();
}
}
function launchApp(exec) {
// Try to find the desktop entry
var app = AppSearchService.getAppByExec(exec);
if (app) {
app.execute();
} else {
// Fallback to direct execution
var cleanExec = exec.replace(/%[fFuU]/g, "").trim();
console.log("Launching app directly:", cleanExec);
Quickshell.execDetached(["sh", "-c", cleanExec]);
}
}
function show() {
appDrawerPopout.isVisible = true;
searchField.enabled = true;
searchDebounceTimer.stop(); // Stop any pending search
updateFilteredModel();
appLauncher.searchQuery = "";
}
function hide() {
searchField.enabled = false; // Disable before hiding to prevent Wayland warnings
appDrawerPopout.isVisible = false;
searchDebounceTimer.stop(); // Stop any pending search
searchField.text = "";
showCategories = false;
}
@@ -173,14 +45,6 @@ PanelWindow {
WlrLayershell.namespace: "quickshell-launcher"
visible: isVisible
color: "transparent"
Component.onCompleted: {
var allCategories = AppSearchService.getAllCategories();
// Insert "Recents" after "All"
categories = ["All", "Recents"].concat(allCategories.filter((cat) => {
return cat !== "All";
}));
updateFilteredModel();
}
// Full screen overlay setup for proper focus
anchors {
@@ -190,17 +54,15 @@ PanelWindow {
bottom: true
}
// Search debouncing
Timer {
id: searchDebounceTimer
interval: 50
repeat: false
onTriggered: updateFilteredModel()
}
ListModel {
id: filteredModel
// App launcher logic
AppLauncher {
id: appLauncher
viewMode: Prefs.appLauncherViewMode
gridColumns: 4
onAppLaunched: appDrawerPopout.hide()
onViewModeSelected: Prefs.setAppLauncherViewMode(mode)
}
// Background dim with click to close
@@ -329,19 +191,19 @@ PanelWindow {
appDrawerPopout.hide();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
selectNext();
appLauncher.selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
selectPrevious();
appLauncher.selectPrevious();
event.accepted = true;
} else if (event.key === Qt.Key_Right && viewMode === "grid") {
selectNextInRow();
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow();
event.accepted = true;
} else if (event.key === Qt.Key_Left && viewMode === "grid") {
selectPreviousInRow();
} 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) {
launchSelected();
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
@@ -378,7 +240,7 @@ PanelWindow {
// Quick stats
Text {
anchors.verticalCenter: parent.verticalCenter
text: filteredModel.count + " apps"
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
@@ -404,21 +266,15 @@ PanelWindow {
enabled: appDrawerPopout.isVisible
placeholderText: "Search applications..."
onTextEdited: {
searchDebounceTimer.restart();
appLauncher.searchQuery = text;
}
Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count && text.length > 0) {
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 = filteredModel.get(0);
if (firstApp.desktopEntry) {
Prefs.addRecentApp(firstApp.desktopEntry);
firstApp.desktopEntry.execute();
} else {
appDrawerPopout.launchApp(firstApp.exec);
}
appDrawerPopout.hide();
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 && viewMode === "grid") || (event.key === Qt.Key_Right && viewMode === "grid") || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
} 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;
}
@@ -467,7 +323,7 @@ PanelWindow {
}
Text {
text: selectedCategory
text: appLauncher.selectedCategory
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
@@ -510,12 +366,11 @@ PanelWindow {
circular: false
iconName: "view_list"
iconSize: 20
iconColor: viewMode === "list" ? Theme.primary : Theme.surfaceText
hoverColor: 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: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
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: {
viewMode = "list";
Prefs.setAppLauncherViewMode("list");
appLauncher.setViewMode("list");
}
}
@@ -525,12 +380,11 @@ PanelWindow {
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: viewMode === "grid" ? Theme.primary : Theme.surfaceText
hoverColor: 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: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
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: {
viewMode = "grid";
Prefs.setAppLauncherViewMode("grid");
appLauncher.setViewMode("grid");
}
}
@@ -558,24 +412,18 @@ PanelWindow {
id: appList
anchors.fill: parent
visible: viewMode === "list"
model: filteredModel
currentIndex: selectedIndex
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 72
iconSize: 56
showDescription: true
hoverUpdatesSelection: false
onItemClicked: function(index, modelData) {
if (modelData.desktopEntry) {
Prefs.addRecentApp(modelData.desktopEntry);
modelData.desktopEntry.execute();
} else {
appDrawerPopout.launchApp(modelData.exec);
}
appDrawerPopout.hide();
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
selectedIndex = index;
appLauncher.selectedIndex = index;
}
}
@@ -584,23 +432,17 @@ PanelWindow {
id: appGrid
anchors.fill: parent
visible: viewMode === "grid"
model: filteredModel
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
currentIndex: selectedIndex
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
onItemClicked: function(index, modelData) {
if (modelData.desktopEntry) {
Prefs.addRecentApp(modelData.desktopEntry);
modelData.desktopEntry.execute();
} else {
appDrawerPopout.launchApp(modelData.exec);
}
appDrawerPopout.hide();
appLauncher.launchApp(modelData);
}
onItemHovered: function(index) {
selectedIndex = index;
appLauncher.selectedIndex = index;
}
}
@@ -654,7 +496,7 @@ PanelWindow {
// Make mouse wheel scrolling more responsive
property real wheelStepSize: 60
model: categories
model: appLauncher.categories
spacing: 4
MouseArea {
@@ -686,8 +528,8 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: selectedCategory === modelData ? Theme.primary : Theme.surfaceText
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
color: appLauncher.selectedCategory === modelData ? Theme.primary : Theme.surfaceText
font.weight: appLauncher.selectedCategory === modelData ? Font.Medium : Font.Normal
}
MouseArea {
@@ -697,9 +539,8 @@ PanelWindow {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
appLauncher.setCategory(modelData);
showCategories = false;
updateFilteredModel();
}
}

View File

@@ -0,0 +1,201 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
// Public interface
property string searchQuery: ""
property string selectedCategory: "All"
property string viewMode: "list" // "list" or "grid"
property int selectedIndex: 0
property int maxResults: 50
property int gridColumns: 4
property bool debounceSearch: true
property int debounceInterval: 50
// Categories (computed from AppSearchService)
property var categories: {
var allCategories = AppSearchService.getAllCategories().filter(cat => {
return cat !== "Education" && cat !== "Science";
});
var result = ["All", "Recents"];
return result.concat(allCategories.filter(cat => {
return cat !== "All";
}));
}
// Recent apps helper
property var recentApps: Prefs.recentlyUsedApps.map(recentApp => {
var app = AppSearchService.getAppByExec(recentApp.exec);
return app && !app.noDisplay ? app : null;
}).filter(app => {
return app !== null;
})
// Signals
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
// Internal model
property alias model: filteredModel
ListModel {
id: filteredModel
}
// Search debouncing
Timer {
id: searchDebounceTimer
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
// Watch for changes
onSearchQueryChanged: {
if (debounceSearch) {
searchDebounceTimer.restart();
} else {
updateFilteredModel();
}
}
onSelectedCategoryChanged: updateFilteredModel()
function updateFilteredModel() {
filteredModel.clear();
selectedIndex = 0;
var apps = [];
if (searchQuery.length === 0) {
// Show apps from category
if (selectedCategory === "All") {
apps = AppSearchService.applications || [];
} else if (selectedCategory === "Recents") {
apps = recentApps;
} else {
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
apps = categoryApps.slice(0, maxResults);
}
} else {
// Search with category filter
if (selectedCategory === "All") {
apps = AppSearchService.searchApplications(searchQuery);
} else if (selectedCategory === "Recents") {
if (recentApps.length > 0) {
var allSearchResults = AppSearchService.searchApplications(searchQuery);
var recentNames = new Set(recentApps.map(app => app.name));
apps = allSearchResults.filter(searchApp => {
return recentNames.has(searchApp.name);
});
} else {
apps = [];
}
} else {
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
if (categoryApps.length > 0) {
var allSearchResults = AppSearchService.searchApplications(searchQuery);
var categoryNames = new Set(categoryApps.map(app => app.name));
apps = allSearchResults.filter(searchApp => {
return categoryNames.has(searchApp.name);
}).slice(0, maxResults);
} else {
apps = [];
}
}
}
// Convert to model format and populate
apps.forEach(app => {
if (app) {
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
});
}
});
}
// Keyboard navigation functions
function selectNext() {
if (filteredModel.count > 0) {
if (viewMode === "grid") {
var newIndex = Math.min(selectedIndex + gridColumns, filteredModel.count - 1);
selectedIndex = newIndex;
} else {
selectedIndex = (selectedIndex + 1) % filteredModel.count;
}
}
}
function selectPrevious() {
if (filteredModel.count > 0) {
if (viewMode === "grid") {
var newIndex = Math.max(selectedIndex - gridColumns, 0);
selectedIndex = newIndex;
} else {
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredModel.count - 1;
}
}
}
function selectNextInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
}
}
function selectPreviousInRow() {
if (filteredModel.count > 0 && viewMode === "grid") {
selectedIndex = Math.max(selectedIndex - 1, 0);
}
}
// App launching
function launchSelected() {
if (filteredModel.count > 0 && selectedIndex >= 0 && selectedIndex < filteredModel.count) {
var selectedApp = filteredModel.get(selectedIndex);
launchApp(selectedApp);
}
}
function launchApp(appData) {
if (appData.desktopEntry) {
Prefs.addRecentApp(appData.desktopEntry);
appData.desktopEntry.execute();
} else {
// Fallback to direct execution
var cleanExec = appData.exec.replace(/%[fFuU]/g, "").trim();
console.log("AppLauncher: Launching app directly:", cleanExec);
Quickshell.execDetached(["sh", "-c", cleanExec]);
}
appLaunched(appData);
}
// Category management
function setCategory(category) {
selectedCategory = category;
categorySelected(category);
}
// View mode management
function setViewMode(mode) {
viewMode = mode;
viewModeSelected(mode);
}
// Initialize
Component.onCompleted: {
updateFilteredModel();
}
}

View File

@@ -0,0 +1,143 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Item {
id: root
property var categories: []
property string selectedCategory: "All"
property bool compact: false // For different layout styles
signal categorySelected(string category)
height: compact ? 36 : (72 + Theme.spacingS) // Single row vs two rows
// Compact single-row layout (for SpotlightModal style)
Row {
visible: compact
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories.slice(0, Math.min(categories.length, 8)) // Limit for space
Rectangle {
height: 36
width: (parent.width - (Math.min(categories.length, 8) - 1) * Theme.spacingS) / Math.min(categories.length, 8)
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
}
}
// Two-row layout (for SpotlightModal organized style)
Column {
visible: !compact
width: parent.width
spacing: Theme.spacingS
// Top row: All, Recents, Development, Graphics (4 items)
Row {
property var topRowCategories: ["All", "Recents", "Development", "Graphics"]
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.topRowCategories.filter(cat => {
return categories.includes(cat);
})
Rectangle {
height: 36
width: (parent.width - (parent.topRowCategories.length - 1) * Theme.spacingS) / parent.topRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
}
}
// Bottom row: Internet, Media, Office, Settings, System (5 items)
Row {
property var bottomRowCategories: ["Internet", "Media", "Office", "Settings", "System"]
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.bottomRowCategories.filter(cat => {
return categories.includes(cat);
})
Rectangle {
height: 36
width: (parent.width - (parent.bottomRowCategories.length - 1) * Theme.spacingS) / parent.bottomRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
}
}
}
}

View File

@@ -97,6 +97,44 @@ PanelWindow {
}
}
// Only resize after animation is complete
onOpacityChanged: {
if (opacity === 1) {
// Animation finished, now we can safely resize
Qt.callLater(() => {
height = calculateHeight();
});
}
}
Connections {
function onEventsByDateChanged() {
if (mainContainer.opacity === 1) {
mainContainer.height = mainContainer.calculateHeight();
}
}
function onKhalAvailableChanged() {
if (mainContainer.opacity === 1) {
mainContainer.height = mainContainer.calculateHeight();
}
}
target: CalendarService
enabled: CalendarService !== null
}
Connections {
function onSelectedDateEventsChanged() {
if (mainContainer.opacity === 1) {
mainContainer.height = mainContainer.calculateHeight();
}
}
target: events
enabled: events !== null
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
@@ -122,27 +160,6 @@ PanelWindow {
}
Connections {
function onEventsByDateChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
function onKhalAvailableChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
target: CalendarService
enabled: CalendarService !== null
}
Connections {
function onSelectedDateEventsChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
target: events
enabled: events !== null
}
Column {

View File

@@ -8,13 +8,13 @@ import qs.Common
PanelWindow {
id: root
property bool showTrayMenu: false
property real trayMenuX: 0
property real trayMenuY: 0
property bool showContextMenu: false
property real contextMenuX: 0
property real contextMenuY: 0
property var currentTrayMenu: null
property var currentTrayItem: null
visible: showTrayMenu
visible: showContextMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -30,8 +30,8 @@ PanelWindow {
Rectangle {
id: menuContainer
x: trayMenuX
y: trayMenuY
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()
@@ -39,8 +39,8 @@ PanelWindow {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
// Material 3 animations
opacity: showTrayMenu ? 1 : 0
scale: showTrayMenu ? 1 : 0.85
opacity: showContextMenu ? 1 : 0
scale: showContextMenu ? 1 : 0.85
// Material 3 drop shadow
Rectangle {
@@ -139,7 +139,7 @@ PanelWindow {
if (modelData.triggered)
modelData.triggered();
showTrayMenu = false;
showContextMenu = false;
}
}
@@ -180,7 +180,7 @@ PanelWindow {
anchors.fill: parent
z: -1
onClicked: {
showTrayMenu = false;
showContextMenu = false;
}
}

View File

@@ -194,11 +194,11 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
visible: Prefs.showSystemTray
onMenuRequested: (menu, item, x, y) => {
trayMenuPopup.currentTrayMenu = menu;
trayMenuPopup.currentTrayItem = item;
trayMenuPopup.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
trayMenuPopup.trayMenuY = Theme.barHeight - Theme.spacingXS;
trayMenuPopup.showTrayMenu = true;
systemTrayContextMenu.currentTrayMenu = menu;
systemTrayContextMenu.currentTrayItem = item;
systemTrayContextMenu.contextMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
systemTrayContextMenu.contextMenuY = Theme.barHeight - Theme.spacingXS;
systemTrayContextMenu.showContextMenu = true;
menu.menuVisible = true;
}
}

View File

@@ -2,13 +2,13 @@
import Quickshell
import qs.Modules
import qs.Modules.AppDrawer
import qs.Modules.CentcomCenter
import qs.Modules.ControlCenter
import qs.Modules.Settings
import qs.Modules.TopBar
import qs.Modules.ProcessList
import qs.Modules.ControlCenter.Network
import qs.Modules.Popouts
import qs.Modals
ShellRoot {
@@ -29,8 +29,8 @@ ShellRoot {
id: centcomPopout
}
TrayMenuPopup {
id: trayMenuPopup
SystemTrayContextMenu {
id: systemTrayContextMenu
}
NotificationCenter {
@@ -63,8 +63,8 @@ ShellRoot {
id: batteryPopout
}
PowerMenuPopup {
id: powerMenuPopup
PowerMenu {
id: powerMenu
}
PowerConfirmModal {