diff --git a/Bar/Bar.qml b/Bar/Bar.qml index 201e336..67c20bf 100644 --- a/Bar/Bar.qml +++ b/Bar/Bar.qml @@ -63,6 +63,10 @@ Scope { Media { anchors.verticalCenter: parent.verticalCenter } + + Taskbar { + anchors.verticalCenter: parent.verticalCenter + } } ActiveWindow { diff --git a/Bar/Modules/Battery.qml b/Bar/Modules/Battery.qml index fe66910..6ca0848 100644 --- a/Bar/Modules/Battery.qml +++ b/Bar/Modules/Battery.qml @@ -61,17 +61,17 @@ Item { id: batteryTooltip text: { let lines = []; - if (isReady) { - lines.push(charging ? "Charging" : "Discharging"); - lines.push(Math.round(percent) + "%"); - if (battery.changeRate !== undefined) - lines.push("Rate: " + battery.changeRate.toFixed(2) + " W"); - if (battery.timeToEmpty > 0) - lines.push("Time left: " + Math.floor(battery.timeToEmpty / 60) + " min"); - if (battery.timeToFull > 0) - lines.push("Time to full: " + Math.floor(battery.timeToFull / 60) + " min"); - if (battery.healthPercentage !== undefined) - lines.push("Health: " + Math.round(battery.healthPercentage) + "%"); + if (batteryWidget.isReady) { + lines.push(batteryWidget.charging ? "Charging" : "Discharging"); + lines.push(Math.round(batteryWidget.percent) + "%"); + if (batteryWidget.battery.changeRate !== undefined) + lines.push("Rate: " + batteryWidget.battery.changeRate.toFixed(2) + " W"); + if (batteryWidget.battery.timeToEmpty > 0) + lines.push("Time left: " + Math.floor(batteryWidget.battery.timeToEmpty / 60) + " min"); + if (batteryWidget.battery.timeToFull > 0) + lines.push("Time to full: " + Math.floor(batteryWidget.battery.timeToFull / 60) + " min"); + if (batteryWidget.battery.healthPercentage !== undefined) + lines.push("Health: " + Math.round(batteryWidget.battery.healthPercentage) + "%"); } return lines.join("\n"); } diff --git a/Bar/Modules/Taskbar.qml b/Bar/Modules/Taskbar.qml new file mode 100644 index 0000000..ec87073 --- /dev/null +++ b/Bar/Modules/Taskbar.qml @@ -0,0 +1,185 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Settings +import qs.Components + +Item { + id: taskbar + width: runningAppsRow.width + height: Settings.settings.taskbarIconSize + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) + return ""; + + // Try different icon resolution strategies + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); + if (!icon) { + icon = Quickshell.iconPath(toplevel.appId, true); + } + if (!icon) { + icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); + } + if (!icon) { + icon = Quickshell.iconPath(toplevel.title, true); + } + if (!icon) { + icon = Quickshell.iconPath("application-x-executable", true); + } + + return icon || ""; + } + + Row { + id: runningAppsRow + spacing: 8 + height: parent.height + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + + id: appButton + width: Settings.settings.taskbarIconSize + height: Settings.settings.taskbarIconSize + radius: Math.max(4, Settings.settings.taskbarIconSize * 0.25) + color: isActive ? Theme.accentPrimary : (hovered ? Theme.surfaceVariant : "transparent") + border.color: isActive ? Qt.darker(Theme.accentPrimary, 1.2) : "transparent" + border.width: 1 + + + + property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData + property bool hovered: mouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + Behavior on color { + ColorAnimation { + duration: 150 + } + } + + Behavior on border.color { + ColorAnimation { + duration: 150 + } + } + + // App icon + IconImage { + id: appIcon + width: Math.max(12, Settings.settings.taskbarIconSize * 0.625) // 62.5% of button size (20/32 = 0.625) + height: Math.max(12, Settings.settings.taskbarIconSize * 0.625) + anchors.centerIn: parent + source: getAppIcon(modelData) + smooth: true + + // Fallback to first letter if no icon + visible: source.toString() !== "" + } + + // Fallback text if no icon available + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.family: Theme.fontFamily + font.pixelSize: Math.max(10, Settings.settings.taskbarIconSize * 0.4375) // 43.75% of button size (14/32 = 0.4375) + font.bold: true + color: appButton.isActive ? Theme.onAccent : Theme.textPrimary + } + + // Tooltip + ToolTip { + id: tooltip + visible: mouseArea.containsMouse && !mouseArea.pressed + delay: 800 + text: appTitle || appId + + background: Rectangle { + color: Theme.backgroundPrimary + border.color: Theme.outline + border.width: 1 + radius: 8 + } + + contentItem: Text { + text: tooltip.text + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeCaption + color: Theme.textPrimary + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: function(mouse) { + console.log("[Taskbar] Clicked on", appButton.appId, "- Active:", appButton.isActive); + + if (mouse.button === Qt.MiddleButton) { + console.log("[Taskbar] Middle-clicked on", appButton.appId); + + // Example: Close the window with middle click + if (modelData && modelData.close) { + modelData.close(); + } else { + console.log("[Taskbar] No close method available for:", modelData); + } + } + + if (mouse.button === Qt.LeftButton) { + // Left click: Focus/activate the window + if (modelData && modelData.activate) { + modelData.activate(); + } else { + console.log("[Taskbar] No activate method available for:", modelData); + } + } + } + + // Right-click for additional actions + onPressed: mouse => { + if (mouse.button === Qt.RightButton) { + console.log("[Taskbar] Right-clicked on", appButton.appId); + + // Example actions you can add: + // 1. Close window + // if (modelData && modelData.close) { + // modelData.close(); + // } + + // 2. Minimize window + // if (modelData && modelData.minimize) { + // modelData.minimize(); + // } + + // 3. Show context menu (needs Menu component) + // contextMenu.popup(); + } + } + } + + // Active indicator dot + Rectangle { + visible: isActive + width: 4 + height: 4 + radius: 2 + color: Theme.onAccent + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: -6 + } + } + } + } +} diff --git a/Helpers/IPCHandlers.qml b/Helpers/IPCHandlers.qml index 9e72395..6cc0aea 100644 --- a/Helpers/IPCHandlers.qml +++ b/Helpers/IPCHandlers.qml @@ -1,11 +1,25 @@ import Quickshell.Io +import "./IdleInhibitor.qml" + IpcHandler { property var appLauncherPanel property var lockScreen + property IdleInhibitor idleInhibitor + property var notificationPopup target: "globalIPC" + function toggleIdleInhibitor(): void { + idleInhibitor.toggle() + } + + + function toggleNotificationPopup(): void { + console.log("[IPC] NotificationPopup toggle() called") + notificationPopup.togglePopup(); + } + // Toggle Applauncher visibility function toggleLauncher(): void { if (!appLauncherPanel) { diff --git a/Helpers/IdleInhibitor.qml b/Helpers/IdleInhibitor.qml new file mode 100644 index 0000000..de78a99 --- /dev/null +++ b/Helpers/IdleInhibitor.qml @@ -0,0 +1,43 @@ +import Quickshell.Io + +Process { + id: idleRoot + + // Example: systemd-inhibit to prevent idle/sleep + command: ["systemd-inhibit", "--what=idle:sleep", "--who=noctalia", "--why=User requested", "sleep", "infinity"] + + // Keep process running in background + property bool isRunning: running + + onStarted: { + console.log("[IdleInhibitor] Process started - idle inhibited") + } + + onExited: function(exitCode, exitStatus) { + console.log("[IdleInhibitor] Process finished:", exitCode) + } + + // Control functions + function start() { + if (!running) { + console.log("[IdleInhibitor] Starting idle inhibitor...") + running = true + } + } + + function stop() { + if (running) { + // Force stop the process by setting running to false + running = false + console.log("[IdleInhibitor] Stopping idle inhibitor...") + } + } + + function toggle() { + if (running) { + stop() + } else { + start() + } + } +} diff --git a/README.md b/README.md index 40c4634..473994d 100644 --- a/README.md +++ b/README.md @@ -153,14 +153,23 @@ To make the weather widget, wallpaper manager and record button work you will ha qs ipc call globalIPC toggleLauncher ``` -### Toggle Lockscreen: +### Toggle Notification Popup: ``` -qs ipc call globalIPC toggleLock +qs ipc call globalIPC toggleNotificationPopup ``` -You can keybind it however you want in your niri setup. +### Toggle Idle Inhibitor: +``` +qs ipc call globalIPC toggleIdleInhibitor +``` + +### Toggle Fullscreen: + +``` +qs ipc call globalIPC toggleFullscreen +``` --- diff --git a/Settings/Settings.qml b/Settings/Settings.qml index fb471d6..541ce59 100644 --- a/Settings/Settings.qml +++ b/Settings/Settings.qml @@ -7,8 +7,9 @@ import qs.Services Singleton { property string shellName: "Noctalia" - property string settingsDir: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" - property string settingsFile: settingsDir + "Settings.json" + property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/" + property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (settingsDir + "Settings.json") + property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE") || (settingsDir + "Theme.json") property var settings: settingAdapter Item { @@ -60,6 +61,8 @@ Singleton { property bool reverseDayMonth: false property bool use12HourClock: false property bool dimPanels: true + property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller) + property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40) property var pinnedExecs: [] // Added for AppLauncher pinned apps } } diff --git a/Settings/Theme.qml b/Settings/Theme.qml index c3b5394..74f3afc 100644 --- a/Settings/Theme.qml +++ b/Settings/Theme.qml @@ -15,13 +15,13 @@ Singleton { // FileView to load theme data from JSON file FileView { id: themeFile - path: Settings.settingsDir + "Theme.json" + path: Settings.themeFile watchChanges: true onFileChanged: reload() onAdapterUpdated: writeAdapter() onLoadFailed: function(error) { - if (error.includes("No such file")) { - themeData = {} + if (error.toString().includes("No such file") || error === 2) { + // File doesn't exist, create it with default values writeAdapter() } } @@ -102,9 +102,13 @@ Singleton { // Font Properties property string fontFamily: "Roboto" // Family for all text - - property int fontSizeHeader: 32 // Headers and titles - property int fontSizeBody: 16 // Body text and general content - property int fontSizeSmall: 14 // Small text like clock, labels - property int fontSizeCaption: 12 // Captions and fine print + + // Font size multiplier - adjust this in Settings.json to scale all fonts + property real fontSizeMultiplier: Settings.settings.fontSizeMultiplier || 1.0 + + // Base font sizes (multiplied by fontSizeMultiplier) + property int fontSizeHeader: Math.round(32 * fontSizeMultiplier) // Headers and titles + property int fontSizeBody: Math.round(16 * fontSizeMultiplier) // Body text and general content + property int fontSizeSmall: Math.round(14 * fontSizeMultiplier) // Small text like clock, labels + property int fontSizeCaption: Math.round(12 * fontSizeMultiplier) // Captions and fine print } diff --git a/Widgets/Notification/NotificationIcon.qml b/Widgets/Notification/NotificationIcon.qml index 6af48c6..988483a 100644 --- a/Widgets/Notification/NotificationIcon.qml +++ b/Widgets/Notification/NotificationIcon.qml @@ -7,12 +7,20 @@ import qs.Components Item { id: root width: 22; height: 22 + property bool isSilence: false + + // Process for executing CLI commands + Process { + id: rightClickProcess + command: ["qs","ipc", "call", "globalIPC", "toggleNotificationPopup"] + } // Bell icon/button Item { id: bell width: 22; height: 22 Text { + id: bellText anchors.centerIn: parent text: notificationHistoryWin.hasUnread ? "notifications_unread" : "notifications" font.family: mouseAreaBell.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined" @@ -25,7 +33,20 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: notificationHistoryWin.visible = !notificationHistoryWin.visible + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: function(mouse): void { + if (mouse.button === Qt.RightButton) { + root.isSilence = !root.isSilence; + rightClickProcess.running = true; + bellText.text = root.isSilence ? "notifications_off" : "notifications" + } + + if (mouse.button === Qt.LeftButton){ + notificationHistoryWin.visible = !notificationHistoryWin.visible + return; + } + + } onEntered: notificationTooltip.tooltipVisible = true onExited: notificationTooltip.tooltipVisible = false } diff --git a/Widgets/Notification/NotificationPopup.qml b/Widgets/Notification/NotificationPopup.qml index 2b30455..42ad0ba 100644 --- a/Widgets/Notification/NotificationPopup.qml +++ b/Widgets/Notification/NotificationPopup.qml @@ -8,11 +8,12 @@ PanelWindow { implicitWidth: 350 implicitHeight: notificationColumn.implicitHeight color: "transparent" - visible: notificationModel.count > 0 + visible: notificationsVisible && notificationModel.count > 0 screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null focusable: false property bool barVisible: true + property bool notificationsVisible: true anchors.top: true anchors.right: true @@ -26,6 +27,12 @@ PanelWindow { property int maxVisible: 5 property int spacing: 5 + function togglePopup(): void { + console.log("[NotificationPopup] Current state: " + notificationsVisible); + notificationsVisible = !notificationsVisible; + console.log("[NotificationPopup] New state: " + notificationsVisible); + } + function addNotification(notification) { notificationModel.insert(0, { id: notification.id, diff --git a/shell.qml b/shell.qml index 0a577d0..8cb676c 100644 --- a/shell.qml +++ b/shell.qml @@ -11,6 +11,9 @@ import qs.Widgets.Notification import qs.Settings import qs.Helpers +import "./Helpers/IdleInhibitor.qml" +import "./Helpers/IPCHandlers.qml" + Scope { id: root @@ -50,12 +53,18 @@ Scope { } } + IdleInhibitor { + id: idleInhibitor + } + NotificationServer { id: notificationServer onNotification: function (notification) { console.log("Notification received:", notification.appName); notification.tracked = true; - notificationPopup.addNotification(notification); + if (notificationPopup.notificationsVisible) { + notificationPopup.addNotification(notification); + } if (notificationHistoryWin) { notificationHistoryWin.addToHistory({ id: notification.id, @@ -89,6 +98,8 @@ Scope { IPCHandlers { appLauncherPanel: appLauncherPanel lockScreen: lockScreen + idleInhibitor: idleInhibitor + notificationPopup: notificationPopup } Connections {