Build the Panama Hyprland desktop
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
// One row of clipboard history: what was copied, where it came from, and how
|
||||
// long ago. Clicking it puts the entry back on the clipboard.
|
||||
//
|
||||
// Rows are cards on an already-glass surface, so they get no prism edge — the
|
||||
// hairline marks a pane of glass, not every item sitting on one.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var entry
|
||||
|
||||
signal activated
|
||||
|
||||
implicitHeight: Math.max(52, body.implicitHeight + 18)
|
||||
|
||||
radius: Theme.cardRadius
|
||||
// border.width: 0 works around QTBUG-137166, where transparent rounded
|
||||
// rectangles can render with holes at the corners.
|
||||
border.width: 0
|
||||
|
||||
color: {
|
||||
if (mouse.pressed)
|
||||
return Theme.alpha(Theme.accent, Theme.activeAlpha);
|
||||
if (mouse.containsMouse)
|
||||
return Theme.alpha(Theme.fg, Theme.hoverAlpha);
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
// Fast in, relaxed out — the row should feel like it answers the pointer
|
||||
// rather than fading after it.
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: mouse.containsMouse ? Theme.durFast : Theme.durNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Nerd Font glyph for the kind of thing that was copied. Text is the common
|
||||
// case and gets the quietest icon, so the odd link or image stands out.
|
||||
readonly property string kindGlyph: {
|
||||
switch (root.entry.kind) {
|
||||
case Clipboard.kindLink:
|
||||
return "\u{F0C1}"; // nf-fa-link
|
||||
case Clipboard.kindImage:
|
||||
return "\u{F03E}"; // nf-fa-picture_o
|
||||
case Clipboard.kindFile:
|
||||
return "\u{F016}"; // nf-fa-file_o
|
||||
default:
|
||||
return "\u{F15C}"; // nf-fa-file_text_o
|
||||
}
|
||||
}
|
||||
|
||||
// Where it came from. Vicinae leaves `source` empty for clients it cannot
|
||||
// attribute (anything pasted by a script, for one), in which case a link's
|
||||
// host is more informative than nothing.
|
||||
readonly property string origin: root.entry.source || root.entry.host || ""
|
||||
|
||||
Text {
|
||||
id: glyph
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 11
|
||||
text: root.entry.encrypted ? "\u{F023}" : root.kindGlyph
|
||||
// fontMono is the Nerd Font, used here only to draw an icon.
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 14
|
||||
color: root.entry.pinned ? Theme.accent : Theme.fgMuted
|
||||
}
|
||||
|
||||
Column {
|
||||
id: body
|
||||
anchors.left: glyph.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: timeLabel.left
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.entry.encrypted ? "Concealed by Vicinae" : root.entry.preview
|
||||
color: root.entry.encrypted ? Theme.fgMuted : Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.italic: root.entry.encrypted
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 6
|
||||
visible: chip.visible || originLabel.visible
|
||||
|
||||
// Images and file drags have no text worth reading, so the type is
|
||||
// spelled out rather than left to the glyph alone.
|
||||
Rectangle {
|
||||
id: chip
|
||||
visible: !root.entry.textual
|
||||
width: chipLabel.implicitWidth + 12
|
||||
height: chipLabel.implicitHeight + 4
|
||||
radius: 4
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.10)
|
||||
|
||||
Text {
|
||||
id: chipLabel
|
||||
anchors.centerIn: parent
|
||||
text: root.entry.typeLabel
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: originLabel
|
||||
anchors.verticalCenter: chip.verticalCenter
|
||||
visible: root.origin !== ""
|
||||
text: root.origin
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: timeLabel
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 11
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.entry.pinned
|
||||
text: "\u{F0403}" // nf-md-pin
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 11
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Clipboard.relativeTime(root.entry.ts)
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
// The list is full of these stacked in a column; tabular figures
|
||||
// keep them from stepping sideways row to row.
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activated()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// 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
|
||||
}
|
||||
margins {
|
||||
top: Theme.barHeight + 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// The bar entry for clipboard history. The icon is intentionally familiar and
|
||||
// quiet: the history itself is a utility, not another permanent status readout.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
|
||||
implicitWidth: 30
|
||||
implicitHeight: 28
|
||||
|
||||
onActivated: ShellState.toggle("clipboard")
|
||||
|
||||
ThemedIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 16
|
||||
icon: "edit-paste-symbolic"
|
||||
iconFallback: "edit-copy-symbolic"
|
||||
tint: ShellState.clipboardOpen ? Theme.accent : Theme.fg
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// The filter box at the top of the clipboard popover. Nothing is submitted:
|
||||
// the text is read live and narrows the list that is already loaded, which is
|
||||
// how GNOME's own search entries behave.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias text: input.text
|
||||
property string placeholder: "Search"
|
||||
|
||||
signal accepted
|
||||
signal dismissed
|
||||
|
||||
implicitHeight: 32
|
||||
radius: Theme.pillRadius
|
||||
// border.width is set below rather than left at 0, so the focus ring has
|
||||
// something to change. See QTBUG-137166 for why 0 matters elsewhere.
|
||||
color: Theme.alpha(Theme.fg, 0.07)
|
||||
border.width: 1
|
||||
border.color: input.activeFocus ? Theme.alpha(Theme.accent, 0.55) : "transparent"
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
|
||||
function clear(): void {
|
||||
input.text = "";
|
||||
}
|
||||
|
||||
function grab(): void {
|
||||
input.forceActiveFocus();
|
||||
}
|
||||
|
||||
Text {
|
||||
id: glyph
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "\u{F002}" // nf-fa-search — fontMono is the Nerd Font, icon only
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: 12
|
||||
color: input.activeFocus ? Theme.accent : Theme.fgMuted
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: input
|
||||
anchors.left: glyph.right
|
||||
anchors.leftMargin: 9
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
color: Theme.fg
|
||||
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
||||
selectedTextColor: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
clip: true
|
||||
|
||||
onAccepted: root.accepted()
|
||||
|
||||
// Escape clears a non-empty filter first and only closes the popover on
|
||||
// a second press — losing a typed query to a stray Escape is worse than
|
||||
// one extra keystroke.
|
||||
Keys.onEscapePressed: {
|
||||
if (input.text !== "")
|
||||
input.text = "";
|
||||
else
|
||||
root.dismissed();
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: input.text === ""
|
||||
text: root.placeholder
|
||||
color: Theme.fgMuted
|
||||
font: input.font
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module qs.modules.clipboard
|
||||
ClipboardEntry 1.0 ClipboardEntry.qml
|
||||
ClipboardPanel 1.0 ClipboardPanel.qml
|
||||
ClipboardWidget 1.0 ClipboardWidget.qml
|
||||
SearchField 1.0 SearchField.qml
|
||||
Reference in New Issue
Block a user