The control center fix applies to five more surfaces: the date menu, the clipboard panel, the activity panel, notification toasts, and the signal glass all opened 48 pixels under the bar while their own expression asked for 12. wlr-layer-shell has three behaviors and only the middle one is subtle. A positive exclusiveZone reserves space; a negative one ignores what others reserved; zero reserves nothing but RESPECTS what others reserved. Every popover here uses zero, so the compositor had already placed them below the bar before their own margin applied, and adding Theme.barHeight counted the bar twice. It is not a crash or a warning -- the surface simply opens lower than written -- so a contract now holds the rule mechanically: a surface with exclusiveZone 0 may not name Theme.barHeight in a margin. It also checks the premise it rests on, and fails if the bar ever stops reserving its own height rather than quietly checking the wrong thing. Measured after: date menu and clipboard at 12px, control center at 2px, each matching what its code asks for. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
234 lines
7.0 KiB
QML
234 lines
7.0 KiB
QML
// Clipboard history, backed by Vicinae's existing database. Opening the panel
|
|
// performs one read-only query; nothing polls or repaints while it is closed.
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Hyprland
|
|
import qs.config
|
|
import qs.services
|
|
import qs.widgets
|
|
import qs.modules.quicksettings
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
visible: ShellState.clipboardOpen
|
|
color: "transparent"
|
|
|
|
anchors {
|
|
top: true
|
|
right: true
|
|
}
|
|
// The gap ALONE, not the bar height plus the gap -- see the note in
|
|
// QuickSettings.qml: exclusiveZone 0 already places this below the bar's
|
|
// reserved zone, so adding the bar height counted it twice.
|
|
margins {
|
|
top: Theme.barGap * 2
|
|
right: Theme.barSideMargin
|
|
}
|
|
|
|
exclusiveZone: 0
|
|
implicitWidth: 430
|
|
implicitHeight: 550
|
|
|
|
WlrLayershell.namespace: "qs-popover-clipboard"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
|
|
|
property string filter: ""
|
|
|
|
readonly property var filteredEntries: Clipboard.entries.filter(entry => {
|
|
const needle = root.filter.trim().toLowerCase();
|
|
if (!needle)
|
|
return true;
|
|
return [entry.preview, entry.source, entry.host, entry.typeLabel]
|
|
.join(" ").toLowerCase().includes(needle);
|
|
})
|
|
|
|
function close(): void {
|
|
ShellState.close();
|
|
}
|
|
|
|
function activateFirst(): void {
|
|
if (root.filteredEntries.length === 0)
|
|
return;
|
|
Clipboard.copyEntry(root.filteredEntries[0].id);
|
|
root.close();
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (root.visible) {
|
|
root.filter = "";
|
|
search.clear();
|
|
Clipboard.refresh();
|
|
openAnim.restart();
|
|
Qt.callLater(search.grab);
|
|
}
|
|
}
|
|
|
|
HyprlandFocusGrab {
|
|
windows: [root]
|
|
active: root.visible
|
|
onCleared: root.close()
|
|
}
|
|
|
|
Rectangle {
|
|
id: surface
|
|
anchors.fill: parent
|
|
radius: Theme.popoverRadius
|
|
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
|
border.width: 1
|
|
border.color: Theme.alpha(Theme.fg, 0.08)
|
|
|
|
PrismEdge {
|
|
anchors {
|
|
top: parent.top
|
|
left: parent.left
|
|
right: parent.right
|
|
topMargin: 1
|
|
}
|
|
inset: parent.radius
|
|
}
|
|
|
|
transform: Scale {
|
|
id: openScale
|
|
origin.x: surface.width
|
|
origin.y: 0
|
|
xScale: 1
|
|
yScale: 1
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.popoverPadding
|
|
spacing: Theme.sectionSpacing
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: 30
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 9
|
|
|
|
ThemedIcon {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
size: 17
|
|
icon: "edit-paste-symbolic"
|
|
iconFallback: "edit-copy-symbolic"
|
|
tint: Theme.accent
|
|
}
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "Clipboard"
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeTitle
|
|
font.weight: Font.DemiBold
|
|
}
|
|
}
|
|
|
|
Text {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: Clipboard.entries.length === 1
|
|
? "1 item"
|
|
: Clipboard.entries.length + " items"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.features: Theme.tabularFigures
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
|
|
SearchField {
|
|
id: search
|
|
width: parent.width
|
|
placeholder: "Search clipboard history"
|
|
onTextChanged: root.filter = text
|
|
onAccepted: root.activateFirst()
|
|
onDismissed: root.close()
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 1
|
|
border.width: 0
|
|
color: Theme.alpha(Theme.fg, 0.1)
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: parent.height - y
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: Clipboard.loading
|
|
text: "Loading clipboard history…"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
width: parent.width - 40
|
|
visible: !Clipboard.loading && !Clipboard.available
|
|
horizontalAlignment: Text.AlignHCenter
|
|
wrapMode: Text.Wrap
|
|
text: "Clipboard history is unavailable. Start Vicinae to restore it."
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: !Clipboard.loading && Clipboard.available
|
|
&& root.filteredEntries.length === 0
|
|
text: root.filter ? "No matching clipboard items" : "Nothing copied yet"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
|
|
ScrollColumn {
|
|
anchors.fill: parent
|
|
maxHeight: parent.height
|
|
spacing: 4
|
|
visible: !Clipboard.loading && Clipboard.available
|
|
&& root.filteredEntries.length > 0
|
|
|
|
Repeater {
|
|
model: root.filteredEntries
|
|
|
|
ClipboardEntry {
|
|
required property var modelData
|
|
|
|
width: parent.width
|
|
entry: modelData
|
|
onActivated: {
|
|
Clipboard.copyEntry(modelData.id);
|
|
root.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: openAnim
|
|
target: openScale
|
|
property: "yScale"
|
|
from: 0.96
|
|
to: 1
|
|
duration: Theme.durFast
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|