noctalia
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var pluginApi: null
|
||||
property var screen: null
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Ensures the widget occupies space in the bar
|
||||
implicitWidth: Style.baseWidgetSize
|
||||
implicitHeight: Style.baseWidgetSize
|
||||
|
||||
Layout.preferredWidth: implicitWidth
|
||||
Layout.preferredHeight: implicitHeight
|
||||
|
||||
// Use NIcon instead of NIconButton to avoid circular background with inverted colors
|
||||
NIcon {
|
||||
id: widgetIcon
|
||||
anchors.centerIn: parent
|
||||
icon: "layout-dashboard"
|
||||
// Default icon color: white/light gray, like the others.
|
||||
// Add hover color if the mouse is over.
|
||||
color: mouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (pluginApi) {
|
||||
pluginApi.openPanel(root.screen, root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
IpcHandler {
|
||||
target: "plugin:workspace-overview"
|
||||
|
||||
function toggle() {
|
||||
root.showOverview();
|
||||
}
|
||||
}
|
||||
|
||||
// This function will be exposed
|
||||
function showOverview() {
|
||||
Logger.i("Workspace Overview", "-> Received command to open the Overview");
|
||||
if (pluginApi) {
|
||||
pluginApi.withCurrentScreen(screen => {
|
||||
pluginApi.togglePanel(screen);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("Workspace Overview", "-> [Main] Workspace Overview is ready and listening.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services.UI
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// --- Mandatory Panel Properties (Injected by Noctalia) ---
|
||||
property var pluginApi: null
|
||||
readonly property var geometryPlaceholder: panelContainer
|
||||
readonly property bool allowAttach: true
|
||||
|
||||
// Recommended dimensions adapted to the interface scale
|
||||
property real contentPreferredWidth: 840 * Style.uiScaleRatio
|
||||
property real contentPreferredHeight: Math.max(320 * Style.uiScaleRatio, (workspaceGrid.computedRows * workspaceGrid.cellHeight) + 100 * Style.uiScaleRatio)
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
id: panelContainer
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Style.marginL
|
||||
}
|
||||
spacing: Style.marginL
|
||||
|
||||
// --- Header ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
NText {
|
||||
text: "Workspace Overview"
|
||||
pointSize: Style.fontSizeXL
|
||||
font.weight: Font.Bold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NIconButton {
|
||||
icon: "x"
|
||||
onClicked: {
|
||||
if (pluginApi) {
|
||||
pluginApi.closePanel(pluginApi.panelOpenScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Workspaces Area (Grid) ---
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true // Enable clipping to hide the overflow
|
||||
|
||||
NGridView {
|
||||
id: workspaceGrid
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
// Widen the grid and push its internal scrollbar beyond the clipped parent edge
|
||||
width: parent.width + (40 * Style.uiScaleRatio)
|
||||
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
|
||||
// Use parent.width (the visible area) for cells, not its own widened width
|
||||
cellWidth: parent.width / columns
|
||||
cellHeight: (194.002 + 13.284) * Style.uiScaleRatio
|
||||
|
||||
// The distribution is now handled by the dynamic cellWidth relative to the visible panel
|
||||
property int columns: Math.max(1, Math.min(count, 3))
|
||||
property int computedRows: Math.max(1, Math.ceil(count / columns))
|
||||
|
||||
leftMargin: 0
|
||||
topMargin: Math.max(0, (height - (computedRows * cellHeight)) / 2)
|
||||
|
||||
clip: true
|
||||
|
||||
// Using the real Hyprland model provided by Quickshell
|
||||
model: Hyprland.workspaces
|
||||
|
||||
// --- Workspace Component ---
|
||||
delegate: DropArea {
|
||||
width: workspaceGrid.cellWidth
|
||||
height: workspaceGrid.cellHeight
|
||||
|
||||
// The "required property" tells QML: "I expect the model to send me a modelData"
|
||||
required property var modelData
|
||||
|
||||
// Now you use it directly, without fear of being undefined
|
||||
property int targetWorkspaceId: modelData.id
|
||||
|
||||
// Action when dropping the window in this workspace
|
||||
onDropped: (drop) => {
|
||||
if (drop.hasText && drop.text !== "") {
|
||||
let windowData = JSON.parse(drop.text);
|
||||
Logger.i("Workspace Overview", "Move window " + windowData.winId + " to workspace " + targetWorkspaceId);
|
||||
|
||||
// Hyprland command via Quickshell
|
||||
Hyprland.dispatch("movetoworkspacesilent " + targetWorkspaceId + ",address:" + windowData.winId);
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
id: workspaceBg
|
||||
width: 228.646 * Style.uiScaleRatio // Keep fixed width
|
||||
height: 194.002 * Style.uiScaleRatio // Keep fixed height
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Visual highlight if it is the active workspace or if it contains drag
|
||||
readonly property bool isActiveWorkspace: Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.id === modelData.id
|
||||
|
||||
// Using conditional color while still being an NBox for theme consistency
|
||||
color: isActiveWorkspace ? Color.mPrimary : (parent.containsDrag ? Color.mSurfaceVariant : Color.mSurface)
|
||||
border.color: isActiveWorkspace ? Color.mOnPrimary : Color.mOutline
|
||||
border.width: parent.containsDrag ? 4 : 0
|
||||
opacity: parent.containsDrag ? 0.8 : 1.0
|
||||
|
||||
// MouseArea to click on the workspace and switch to it
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (targetWorkspaceId !== undefined) {
|
||||
Hyprland.dispatch("workspace " + targetWorkspaceId);
|
||||
if (pluginApi) {
|
||||
pluginApi.closePanel(pluginApi.panelOpenScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM / 2
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: modelData.name !== "" ? modelData.name : "Workspace " + modelData.id
|
||||
font.weight: Font.Bold
|
||||
color: workspaceBg.isActiveWorkspace ? Color.mOnPrimary : Color.mOnSurface
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
// List of active windows (Titles)
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 20 * Style.uiScaleRatio
|
||||
spacing: 1 * Style.uiScaleRatio
|
||||
clip: true
|
||||
|
||||
Repeater {
|
||||
model: modelData.toplevels || null
|
||||
delegate: NText {
|
||||
width: 199.663 * Style.uiScaleRatio // Precise width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "• " + (modelData.title || "App")
|
||||
pointSize: 8 * Style.uiScaleRatio
|
||||
elide: Text.ElideRight
|
||||
color: workspaceBg.isActiveWorkspace ? Color.mOnPrimary : Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mini-monitor background (Real wallpaper or solid color)
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.preferredWidth: 199.663 * Style.uiScaleRatio
|
||||
Layout.preferredHeight: 112.256 * Style.uiScaleRatio
|
||||
color: Qt.rgba(0, 0, 0, 0.4)
|
||||
border.color: parent.parent.isActiveWorkspace ? Color.mOnPrimary : Color.mOutline
|
||||
border.width: 2 * Style.uiScaleRatio
|
||||
radius: Style.radiusS
|
||||
clip: true
|
||||
|
||||
// Wallpaper
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: typeof WallpaperService !== "undefined" ? WallpaperService.getWallpaper(modelData.monitor.name) : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: source != ""
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
// Actual dimensions and positions come from the workspace monitor.
|
||||
// Use monitor scale to convert from physical to logical pixels.
|
||||
property var wsMonitor: modelData.monitor || null
|
||||
property real monitorX: wsMonitor ? wsMonitor.x : 0
|
||||
property real monitorY: wsMonitor ? wsMonitor.y : 0
|
||||
property real monitorScale: wsMonitor && wsMonitor.scale > 0 ? wsMonitor.scale : 1.0
|
||||
property real monitorW: (wsMonitor && wsMonitor.width > 0 ? wsMonitor.width : 1920) / monitorScale
|
||||
property real monitorH: (wsMonitor && wsMonitor.height > 0 ? wsMonitor.height : 1080) / monitorScale
|
||||
property real scaleX: width / monitorW
|
||||
property real scaleY: height / monitorH
|
||||
|
||||
Repeater {
|
||||
model: modelData.toplevels || null
|
||||
delegate: Rectangle {
|
||||
// Using 'required property var modelData' forces QML to get the modelData from the internal Repeater
|
||||
// escaping the scope shadowing generated by the external GridView.
|
||||
required property var modelData
|
||||
|
||||
// Geometric information comes from the lastIpcObject associated with the window (inner modelData)
|
||||
property var ipcObj: modelData.lastIpcObject || null
|
||||
property real winX: ipcObj && ipcObj.at ? ipcObj.at[0] : 0
|
||||
property real winY: ipcObj && ipcObj.at ? ipcObj.at[1] : 0
|
||||
property real winW: ipcObj && ipcObj.size ? ipcObj.size[0] : 0
|
||||
property real winH: ipcObj && ipcObj.size ? ipcObj.size[1] : 0
|
||||
|
||||
// Position relative to the monitor
|
||||
x: (winX - parent.monitorX) * parent.scaleX
|
||||
y: (winY - parent.monitorY) * parent.scaleY
|
||||
width: Math.max(2, winW * parent.scaleX)
|
||||
height: Math.max(2, winH * parent.scaleY)
|
||||
|
||||
// Ignore unmapped (hidden) windows
|
||||
visible: modelData.mapped !== undefined ? modelData.mapped : (ipcObj !== null && ipcObj.mapped !== undefined ? ipcObj.mapped : true)
|
||||
|
||||
color: Color.mPrimary
|
||||
border.color: Color.mBackground
|
||||
border.width: Math.max(1, 1 * Style.uiScaleRatio)
|
||||
radius: 2 * Style.uiScaleRatio
|
||||
clip: true
|
||||
|
||||
ScreencopyView {
|
||||
anchors.fill: parent
|
||||
captureSource: modelData.wayland
|
||||
live: true
|
||||
paintCursor: true
|
||||
|
||||
// Optimization: Only capture at the resolution we are displaying
|
||||
constraintSize: Qt.size(parent.width, parent.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Workspace Overview Plugin for Noctalia
|
||||
|
||||
A visually stunning workspace overview with real-time window previews and wallpaper integration, designed perfectly for Hyprland and Noctalia Shell.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Panoramic View**: A balanced 3-column grid that fills your panel with a professional, symmetrical layout.
|
||||
- **Live Previews**: Real-time rendering of all your open windows using `ScreencopyView`.
|
||||
- **Dynamic Sizing**: The panel height automatically adjusts to the number of active workspaces.
|
||||
- **Wallpaper Backgrounds**: Automatically pulls your desktop wallpaper to show "life" in every workspace preview.
|
||||
- **Drag & Drop**: Effortlessly move windows between workspaces by dragging their previews.
|
||||
- **Clean Aesthetic**: No visible scrollbars—just your windows and your work.
|
||||
|
||||
## Installation
|
||||
|
||||
Ensure you have the plugin files in:
|
||||
`~/.config/noctalia/plugins/workspace-overview/`
|
||||
|
||||
## Usage
|
||||
|
||||
### Via Bar Widget
|
||||
Add the "Workspace Overview" widget to your Noctalia bar.
|
||||
|
||||
### Via IPC (Keybindings)
|
||||
You can toggle the overview using the Noctalia IPC interface. This is ideal for assigning to a keyboard shortcut.
|
||||
|
||||
**Command:**
|
||||
```bash
|
||||
qs -c noctalia-shell ipc call plugin:workspace-overview toggle
|
||||
```
|
||||
|
||||
#### Hyprland Keybind Example
|
||||
Add the following to your `hyprland.conf`:
|
||||
```bash
|
||||
bind = SUPER, TAB, exec, qs -c noctalia-shell ipc call plugin:workspace-overview toggle
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Noctalia Shell**: 3.6.0 or later
|
||||
- **Hyprland**: For workspace and window tracking
|
||||
- **Quickshell**: The framework powering Noctalia
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "workspace-overview",
|
||||
"name": "Workspace Overview for Hyprland",
|
||||
"version": "1.0.3",
|
||||
"minNoctaliaVersion": "3.6.0",
|
||||
"author": "doughbran",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "Workspace overview with live window previews and wallpaper backgrounds for Hyprland.",
|
||||
"tags": [
|
||||
"Hyprland",
|
||||
"Bar",
|
||||
"Panel",
|
||||
"Utility"
|
||||
],
|
||||
"entryPoints": {
|
||||
"panel": "Panel.qml",
|
||||
"barWidget": "BarWidget.qml",
|
||||
"main": "Main.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 632 KiB |
Reference in New Issue
Block a user