This commit is contained in:
end-4 2025-04-23 20:40:29 +02:00
parent 806230ff52
commit 41ffd0ac80
13 changed files with 491 additions and 7 deletions

View file

@ -5,4 +5,5 @@ pragma ComponentBehavior: Bound
Singleton {
property int sidebarRightOpenCount: 0
property bool overviewOpen: false
}

View file

@ -30,6 +30,10 @@ Scope {
id: hideOsdVolume
command: ["qs", "ipc", "call", "osdVolume", "hide"]
}
Process {
id: toggleOverview
command: ["qs", "ipc", "call", "overview", "toggle"]
}
Variants { // For each monitor
model: Quickshell.screens
@ -220,6 +224,19 @@ Scope {
}
}
MouseArea { // Middle: right-click to toggle overview
id: barMiddleMouseArea
anchors.fill: middleSection
acceptedButtons: Qt.RightButton
onPressed: (event) => {
if (event.button === Qt.RightButton) {
toggleOverview.running = true;
}
}
}
MouseArea { // Right side: scroll to change volume
id: barRightSideMouseArea
property bool hovered: false
@ -250,7 +267,7 @@ Scope {
if (event.angleDelta.y < 0)
Audio.sink.audio.volume -= step;
else if (event.angleDelta.y > 0)
Audio.sink.audio.volume += step;
Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
// Store the mouse position and start tracking
barRightSideMouseArea.lastScrollX = event.x;
barRightSideMouseArea.lastScrollY = event.y;

View file

@ -145,6 +145,7 @@ Singleton {
property int large: 25
property int full: 9999
property int screenRounding: large
property int windowRounding: 20
}
font: QtObject {

View file

@ -30,6 +30,12 @@ Singleton {
property int timeout: 1000
}
property QtObject overview: QtObject {
property real scale: 0.18 // Relative to screen size
property real numOfRows: 2
property real numOfCols: 5
}
property QtObject resources: QtObject {
property int updateInterval: 3000
}

View file

@ -131,7 +131,6 @@ Item {
if (mouse.button === Qt.LeftButton) {
copyNotificationBody.running = true
notificationSummaryText.text = `${notificationObject.summary} (copied)`
console.log(notificationSummaryText.text)
}
}
onDragStartedChanged: () => {

View file

@ -0,0 +1,105 @@
import "root:/"
import "root:/modules/common"
import "root:/modules/common/widgets"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: overview
Variants {
model: Quickshell.screens
PanelWindow {
id: root
property var modelData
screen: modelData
visible: GlobalStates.overviewOpen
WlrLayershell.namespace: "quickshell:overview"
WlrLayershell.layer: WlrLayer.Overlay
color: "transparent"
mask: Region {
item: columnLayout
}
anchors {
top: true
left: true
right: true
bottom: true
}
HyprlandFocusGrab {
id: grab
windows: [ root ]
active: false
onCleared: () => {
if (!active) GlobalStates.overviewOpen = false
}
}
Connections {
target: root
function onVisibleChanged() {
delayedGrabTimer.start()
}
}
Timer {
id: delayedGrabTimer
interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
repeat: false
onTriggered: {
grab.active = root.visible
}
}
width: columnLayout.width
height: columnLayout.height
ColumnLayout {
id: columnLayout
anchors.horizontalCenter: parent.horizontalCenter
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
sessionRoot.visible = false;
}
}
Item {
height: 1 // Prevent Wayland protocol error
width: 1 // Prevent Wayland protocol error
}
OverviewWidget {
bar: root
}
}
}
}
IpcHandler {
target: "overview"
function toggle() {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen
}
function close() {
GlobalStates.overviewOpen = false
}
function open() {
GlobalStates.overviewOpen = true
}
}
}

View file

@ -0,0 +1,114 @@
import "root:/services/"
import "root:/modules/common"
import "root:/modules/common/widgets"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
import "./icons.js" as Icons
Item {
id: root
required property var bar
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
readonly property var toplevels: ToplevelManager.toplevels
readonly property int workspacesShown: ConfigOptions.overview.numOfRows * ConfigOptions.overview.numOfCols
readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
property var windows: HyprlandData.windowList
property var windowByAddress: HyprlandData.windowByAddress
property var windowAddresses: HyprlandData.addresses
property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id)
property real scale: ConfigOptions.overview.scale
property real workspaceNumberMargin: 80
property real workspaceNumberSize: 80
implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2
property Component windowComponent: OverviewWindow {}
property list<OverviewWindow> windowWidgets: []
// onWindowsChanged: {
// console.log("Windows changed")
// }
Rectangle {
id: overviewBackground
anchors.fill: parent
implicitWidth: columnLayout.implicitWidth + 5 * 2
implicitHeight: columnLayout.implicitHeight + 5 * 2
color: Appearance.colors.colLayer0
radius: Appearance.rounding.screenRounding * root.scale + 5 * 2
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 5
Repeater {
model: ConfigOptions.overview.numOfRows
delegate: RowLayout {
id: row
property int rowIndex: index
Repeater { // Workspace repeater
model: ConfigOptions.overview.numOfCols
Rectangle { // Workspace
id: workspace
property int colIndex: index
property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * ConfigOptions.overview.numOfCols + colIndex + 1
implicitWidth: (monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale
implicitHeight: (monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale
color: Appearance.colors.colLayer1 // TODO: reconsider this color for a cleaner look
radius: Appearance.rounding.screenRounding * root.scale
StyledText {
z: 9999
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: root.workspaceNumberMargin * root.scale
anchors.topMargin: root.workspaceNumberMargin * root.scale
font.pixelSize: root.workspaceNumberSize * root.scale
color: Appearance.colors.colSubtext
text: workspaceValue
}
Repeater { // Window repeater
model: ScriptModel {
values: windowAddresses.filter((address) => {
var win = windowByAddress[address]
return (win?.workspace?.id === workspace.workspaceValue)
})
}
delegate: OverviewWindow {
windowData: windowByAddress[modelData]
monitorData: root.monitorData
scale: root.scale
availableWorkspaceWidth: workspace.implicitWidth
availableWorkspaceHeight: workspace.implicitHeight
}
}
}
}
}
}
}
}
DropShadow {
anchors.fill: overviewBackground
horizontalOffset: 0
verticalOffset: 2
radius: Appearance.sizes.elevationMargin
samples: radius * 2 + 1 // Ideally should be 2 * radius + 1, see qt docs
color: Appearance.colors.colShadow
source: overviewBackground
}
}

View file

@ -0,0 +1,107 @@
import "root:/services/"
import "root:/modules/common"
import "root:/modules/common/widgets"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Hyprland
import "./icons.js" as Icons
Rectangle { // Window
id: root
property var windowData
property var monitorData
property var scale
property var availableWorkspaceWidth
property var availableWorkspaceHeight
property var iconToWindowRatio: 0.35
property var iconToWindowRatioCompact: 0.6
property var iconPath: Quickshell.iconPath(Icons.noKnowledgeIconGuess(windowData?.class))
property bool compactMode: Appearance.font.pixelSize.smaller * 4 > root.height || Appearance.font.pixelSize.smaller * 4 > root.width
z: 1
x: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0)
y: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0)
width: Math.min(windowData?.size[0] * root.scale, availableWorkspaceWidth - x)
height: Math.min(windowData?.size[1] * root.scale, availableWorkspaceHeight - y)
radius: Appearance.rounding.windowRounding * root.scale
color: Appearance.colors.colLayer2
border.color : Appearance.transparentize(Appearance.m3colors.m3outline, 0.9)
border.pixelAligned : false
border.width : 1
Behavior on x {
NumberAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
Behavior on y {
NumberAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
Behavior on width {
NumberAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
Behavior on height {
NumberAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
Process {
id: closeOverview
command: ["bash", "-c", "qs ipc call overview close &"] // Somehow has to by async to work?
}
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: {
if (windowData) {
closeOverview.running = true
Hyprland.dispatch(`focuswindow address:${windowData.address}`)
}
}
}
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.font.pixelSize.smaller * 0.5
IconImage {
id: windowIcon
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: root.width * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
height: root.height * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
}
StyledText {
Layout.leftMargin: 10
Layout.rightMargin: 10
visible: !compactMode
Layout.fillWidth: true
Layout.fillHeight: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.smaller
elide: Text.ElideRight
// wrapMode: Text.Wrap
text: windowData?.title ?? ""
}
}
}

View file

@ -0,0 +1,71 @@
const substitutions = {
"code-url-handler": "visual-studio-code",
"Code": "visual-studio-code",
"GitHub Desktop": "github-desktop",
"Minecraft* 1.20.1": "minecraft",
"gnome-tweaks": "org.gnome.tweaks",
"pavucontrol-qt": "pavucontrol",
"wps": "wps-office2019-kprometheus",
"wpsoffice": "wps-office2019-kprometheus",
"footclient": "foot",
"": "image-missing"
}
const regexSubstitutions = [
{
"regex": "/^steam_app_(\\d+)$/",
"replace": "steam_icon_$1"
}
]
function iconExists(iconName) {
return false; // TODO: Make this work without Gtk
}
function substitute(str) {
// Normal substitutions
if (substitutions[str])
return substitutions[str];
// Regex substitutions
for (let i = 0; i < regexSubstitutions.length; i++) {
const substitution = regexSubstitutions[i];
const replacedName = str.replace(
substitution.regex,
substitution.replace,
);
if (replacedName != str) return replacedName;
}
// Guess: convert to kebab case
if (!iconExists(str)) str = str.toLowerCase().replace(/\s+/g, "-");
// Original string
return str;
}
function noKnowledgeIconGuess(str) {
if (!str) return "image-missing";
// Normal substitutions
if (substitutions[str])
return substitutions[str];
// Regex substitutions
for (let i = 0; i < regexSubstitutions.length; i++) {
const substitution = regexSubstitutions[i];
const replacedName = str.replace(
substitution.regex,
substitution.replace,
);
if (replacedName != str) return replacedName;
}
// Guess: convert to kebab case if it's not reverse domain name notation
if (!str.includes('.')) {
str = str.toLowerCase().replace(/\s+/g, "-");
}
// Original string
return str;
}

View file

@ -187,7 +187,7 @@ Scope {
SessionActionButton {
id: sessionFirmwareReboot
focus: sessionRoot.visible
buttonIcon: "reset_wrench"
buttonIcon: "settings_applications"
buttonText: "Reboot to firmware settings"
onClicked: { firmwareReboot.running = true; sessionRoot.visible = false }
onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }

View file

@ -31,10 +31,6 @@ Button {
}
}
onClicked: {
console.log("Button clicked:", buttonText)
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.full

View file

@ -0,0 +1,65 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Singleton {
id: root
property var windowList: []
property var addresses: []
property var windowByAddress: {}
property var monitors: []
function updateWindowList() {
getClients.running = true
getMonitors.running = true
}
Component.onCompleted: {
updateWindowList()
}
Connections {
target: Hyprland
function onRawEvent(event) {
// Filter out redundant old v1 events for the same thing
if(event in [
"activewindow", "focusedmon", "monitoradded",
"createworkspace", "destroyworkspace", "moveworkspace",
"activespecial", "movewindow", "windowtitle"
]) return ;
updateWindowList()
}
}
Process {
id: getClients
command: ["bash", "-c", "hyprctl clients -j | jq -c"]
stdout: SplitParser {
onRead: (data) => {
root.windowList = JSON.parse(data)
root.windowByAddress = {}
for (var i = 0; i < root.windowList.length; ++i) {
var win = root.windowList[i]
root.windowByAddress[win.address] = win
}
root.addresses = root.windowList.map((win) => win.address)
}
}
}
Process {
id: getMonitors
command: ["bash", "-c", "hyprctl monitors -j | jq -c"]
stdout: SplitParser {
onRead: (data) => {
root.monitors = JSON.parse(data)
}
}
}
}

View file

@ -3,6 +3,7 @@
import "./modules/bar/"
import "./modules/notificationPopup/"
import "./modules/onScreenDisplay/"
import "./modules/overview/"
import "./modules/screenCorners/"
import "./modules/session/"
import "./modules/sidebarRight/"
@ -17,6 +18,7 @@ ShellRoot {
NotificationPopup {}
OnScreenDisplayBrightness {}
OnScreenDisplayVolume {}
Overview {}
ReloadPopup {}
ScreenCorners {}
Session {}