From b0f4607376bec0506b22bf0303011a9d94ff44ce Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 12 Sep 2026 10:20:05 +0200 Subject: feat(mail-overview): the drawer Builds the panel body on the house idiom the other two components already use: a fullscreen transparent PanelWindow holding a dim backdrop, with the real content as a Rectangle anchored inside it. The idiom is worth the seemingly oversized window because it hands us two behaviours for free. Click-outside-to-close is just a MouseArea filling the backdrop, and the drawer only needs its own MouseArea to swallow clicks so they do not fall through. Escape works because the window actually covers the screen and can take exclusive keyboard focus. exclusionMode is Normal here, unlike the Ignore the other two components set. Waybar claims an exclusive zone at the top of this screen, and the mail icon lives in it, so respecting that zone positions the drawer immediately below the bar without this file hardcoding, or even knowing, the bar's height. It also means the backdrop begins below waybar, so the bar itself does not dim while its own drawer is open, which reads correctly: the icon you clicked stays lit. Focus sits on an inner Item with focus: true, not on the window. Setting WlrLayershell.keyboardFocus is necessary but not sufficient, because key events in QML are delivered to a focused item and never to a window, so Keys.onEscapePressed on the PanelWindow would never fire. This cost time in the vm-manager panel and the same shape is used here deliberately. Thread rows are deliberately read-only, with no click handler. qtmaildir takes no arguments, so there is no way to ask it to open a particular thread and a clickable row would promise navigation it cannot deliver. The rows are a preview; the button opens the client at its own idea of where to start. show() refreshes before opening rather than relying on a timer. The panel is opened rarely and the shell is autostarted for the whole session, so the counts are almost always stale by the time anyone looks at them, and a refresh on open is both cheaper and fresher than polling. Also removes the temporary console probe from shell.qml, which existed only to verify the account model parsed and counted correctly. The drawer now displays the same data, so the probe has no job left. Dropping it also drops the QtQuick import, which nothing else in that file needed. Button.qml is an unmodified copy of vm-manager's. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WWL8JYHu7yhAdtx5pU9PMU --- mail-overview/Button.qml | 45 ++++++++++ mail-overview/MailPanel.qml | 213 +++++++++++++++++++++++++++++++++++++++++++- mail-overview/shell.qml | 14 --- 3 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 mail-overview/Button.qml (limited to 'mail-overview') diff --git a/mail-overview/Button.qml b/mail-overview/Button.qml new file mode 100644 index 0000000..d6ca78f --- /dev/null +++ b/mail-overview/Button.qml @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Danilo M. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick + +Rectangle { + id: btn + property alias text: label.text + property bool danger: false + property bool enabled: true + signal clicked + + implicitWidth: label.implicitWidth + 22 + implicitHeight: label.implicitHeight + 12 + radius: 7 + opacity: enabled ? 1 : 0.4 + color: area.containsMouse && enabled + ? Qt.alpha(danger ? Theme.red : Theme.accent, 0.25) + : Qt.alpha(Theme.surface, 0.8) + border.width: 1 + border.color: Qt.alpha(danger ? Theme.red : Theme.text, 0.2) + + Text { + id: label + anchors.centerIn: parent + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: danger ? Theme.red : Theme.text + } + + MouseArea { + id: area + anchors.fill: parent + hoverEnabled: true + cursorShape: btn.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: if (btn.enabled) btn.clicked() + } +} diff --git a/mail-overview/MailPanel.qml b/mail-overview/MailPanel.qml index 7e81a33..7457fb2 100644 --- a/mail-overview/MailPanel.qml +++ b/mail-overview/MailPanel.qml @@ -23,7 +23,12 @@ Scope { readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] - function show() { root.open = true; } + function show() { + // Mail has almost certainly arrived since this was last opened, and + // for an autostarted shell that is the whole session. + Accounts.refresh(); + root.open = true; + } function close() { root.open = false; } function toggle() { root.open ? close() : show(); } @@ -38,4 +43,210 @@ Scope { mask: Region {} WlrLayershell.keyboardFocus: WlrKeyboardFocus.None } + + LazyLoader { + active: root.open + + PanelWindow { + id: win + screen: root.screenObj + + anchors { top: true; left: true; right: true; bottom: true } + color: "transparent" + + // Normal, not Ignore: waybar claims an exclusive zone at the top + // of this screen, so respecting it puts the drawer below the bar + // without this file knowing the bar's height. The backdrop then + // starts below waybar too, which is why waybar stays un-dimmed. + exclusionMode: ExclusionMode.Normal + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "quickshell-mail" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + Rectangle { + anchors.fill: parent + color: "#000000" + opacity: 0.5 + MouseArea { anchors.fill: parent; onClicked: root.close() } + } + + // Keys reach a focused item, never the window itself: setting + // keyboardFocus above is necessary but not sufficient, and + // Keys.onEscapePressed on a PanelWindow never fires. + Item { + anchors.fill: parent + focus: true + Keys.onEscapePressed: root.close() + } + + // Top right, under the waybar icon, which sits in modules-right. + Rectangle { + id: drawer + anchors { top: parent.top; right: parent.right; topMargin: 8; rightMargin: 8 } + width: 460 + height: Math.min(content.implicitHeight + 32, win.height - 24) + radius: 14 + color: Qt.alpha(Theme.base, 0.72) + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.12) + + // Swallows clicks so they do not reach the backdrop and close + // the drawer. + MouseArea { anchors.fill: parent } + + Column { + id: content + anchors { left: parent.left; right: parent.right; top: parent.top; margins: 16 } + spacing: 10 + + Item { + width: parent.width + implicitHeight: Math.max(title.implicitHeight, totalText.implicitHeight) + + Text { + id: title + anchors.verticalCenter: parent.verticalCenter + text: "Mail" + color: Theme.text + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + 2 + font.bold: true + } + + Text { + id: totalText + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + // A dash rather than a possibly-wrong number while + // any account is still unknown. + text: Accounts.anyUnknown ? "—" : `${Accounts.total} unread` + color: Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + } + + Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } + + Text { + visible: Accounts.error !== "" + width: parent.width + text: Accounts.error + color: Theme.red + wrapMode: Text.WordWrap + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 2 + } + + Repeater { + model: Accounts.accounts + + Column { + required property var modelData + width: content.width + spacing: 4 + + Item { + width: parent.width + implicitHeight: 26 + + Rectangle { + id: dot + anchors.verticalCenter: parent.verticalCenter + width: 8; height: 8; radius: 4 + // The account's own colour from the config. + // Per-account identity, not a palette. + color: modelData.color || Theme.accent + } + + Text { + anchors { left: dot.right; leftMargin: 10; verticalCenter: parent.verticalCenter } + text: modelData.label + color: Theme.text + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + + Text { + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + text: modelData.count < 0 ? "—" : String(modelData.count) + color: modelData.count > 0 ? Theme.text : Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + font.bold: modelData.count > 0 + } + } + + // The newest three unread threads. Read-only: + // qtmaildir takes no arguments, so there is no way + // to ask it for a particular thread. + Repeater { + model: modelData.threads + + Column { + required property var modelData + width: content.width - 18 + x: 18 + spacing: 1 + bottomPadding: 4 + + Item { + width: parent.width + implicitHeight: who.implicitHeight + + Text { + id: who + anchors.left: parent.left + width: parent.width - when.implicitWidth - 10 + text: modelData.authors + elide: Text.ElideRight + color: Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 3 + } + + Text { + id: when + anchors.right: parent.right + text: modelData.date + color: Theme.overlay + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 3 + } + } + + Text { + width: parent.width + text: modelData.subject + elide: Text.ElideRight + color: Theme.text + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 2 + } + } + } + } + } + + Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } + + Row { + anchors.right: parent.right + spacing: 10 + + Button { + text: Accounts.syncing ? "Syncing..." : "Sync now" + enabled: !Accounts.syncing + onClicked: Accounts.sync() + } + + Button { + text: "Open qtmaildir" + onClicked: { Accounts.openClient(); root.close(); } + } + } + } + } + } + } } diff --git a/mail-overview/shell.qml b/mail-overview/shell.qml index 7842bc6..cd2e907 100644 --- a/mail-overview/shell.qml +++ b/mail-overview/shell.qml @@ -11,7 +11,6 @@ import Quickshell import Quickshell.Io -import QtQuick ShellRoot { MailPanel { id: panel } @@ -22,17 +21,4 @@ ShellRoot { function show() { panel.show(); } function close() { panel.close(); } } - - // TEMPORARY probe for Task 3 verification. Removed in Step 5. - Component.onCompleted: Qt.callLater(() => { - console.log("ACCOUNTS", JSON.stringify(Accounts.accounts.map(a => a.key + ":" + a.label))); - }) - Connections { - target: Accounts - function onLoadingChanged() { - if (!Accounts.loading) - console.log("COUNTS", JSON.stringify(Accounts.accounts.map(a => a.label + "=" + a.count)), - "total", Accounts.total); - } - } } -- cgit v1.2.3