mirror of
https://github.com/end-4/dots-hyprland.git
synced 2026-06-23 17:29:57 -05:00
Merge branch 'main' of https://github.com/end-4/dots-hyprland into sticky-merge-main
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.widgets.widgetCanvas
|
||||
import qs.modules.common.functions as CF
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import qs.modules.ii.background.widgets
|
||||
import qs.modules.ii.background.widgets.clock
|
||||
import qs.modules.ii.background.widgets.weather
|
||||
|
||||
Variants {
|
||||
id: root
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: bgRoot
|
||||
|
||||
required property var modelData
|
||||
|
||||
// Hide when fullscreen
|
||||
property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace => workspace.monitor && workspace.monitor.name == monitor.name)
|
||||
property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace => ((workspace.toplevels.values.filter(window => window.wayland?.fullscreen)[0] != undefined) && workspace.active))[0]
|
||||
visible: GlobalStates.screenLocked || (!(activeWorkspaceWithFullscreen != undefined)) || !Config?.options.background.hideWhenFullscreen
|
||||
|
||||
// Workspaces
|
||||
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
|
||||
property list<var> relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor?.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id)
|
||||
property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1
|
||||
property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10
|
||||
// Wallpaper
|
||||
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov")
|
||||
property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath
|
||||
property bool wallpaperSafetyTriggered: {
|
||||
const enabled = Config.options.workSafety.enable.wallpaper;
|
||||
const sensitiveWallpaper = (CF.StringUtils.stringListContainsSubstring(wallpaperPath.toLowerCase(), Config.options.workSafety.triggerCondition.fileKeywords));
|
||||
const sensitiveNetwork = (CF.StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords));
|
||||
return enabled && sensitiveWallpaper && sensitiveNetwork;
|
||||
}
|
||||
property real wallpaperToScreenRatio: Math.min(wallpaperWidth / screen.width, wallpaperHeight / screen.height)
|
||||
property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom
|
||||
property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated
|
||||
property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated
|
||||
property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated
|
||||
property real movableXSpace: ((wallpaperWidth / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.width) / 2
|
||||
property real movableYSpace: ((wallpaperHeight / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.height) / 2
|
||||
readonly property bool verticalParallax: (Config.options.background.parallax.autoVertical && wallpaperHeight > wallpaperWidth) || Config.options.background.parallax.vertical
|
||||
// Colors
|
||||
property bool shouldBlur: (GlobalStates.screenLocked && Config.options.lock.blur.enable)
|
||||
property color dominantColor: Appearance.colors.colPrimary // Default, to be changed
|
||||
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
|
||||
property color colText: {
|
||||
if (wallpaperSafetyTriggered)
|
||||
return CF.ColorUtils.mix(Appearance.colors.colOnLayer0, Appearance.colors.colPrimary, 0.75);
|
||||
return (GlobalStates.screenLocked && shouldBlur) ? Appearance.colors.colOnLayer0 : CF.ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12));
|
||||
}
|
||||
Behavior on colText {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
// Layer props
|
||||
screen: modelData
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: (GlobalStates.screenLocked && !scaleAnim.running) ? WlrLayer.Overlay : WlrLayer.Bottom
|
||||
// WlrLayershell.layer: WlrLayer.Bottom
|
||||
WlrLayershell.namespace: "quickshell:background"
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
color: {
|
||||
if (!bgRoot.wallpaperSafetyTriggered || bgRoot.wallpaperIsVideo)
|
||||
return "transparent";
|
||||
return CF.ColorUtils.mix(Appearance.colors.colLayer0, Appearance.colors.colPrimary, 0.75);
|
||||
}
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
onWallpaperPathChanged: {
|
||||
bgRoot.updateZoomScale();
|
||||
// Clock position gets updated after zoom scale is updated
|
||||
}
|
||||
|
||||
// Wallpaper zoom scale
|
||||
function updateZoomScale() {
|
||||
getWallpaperSizeProc.path = bgRoot.wallpaperPath;
|
||||
getWallpaperSizeProc.running = true;
|
||||
}
|
||||
Process {
|
||||
id: getWallpaperSizeProc
|
||||
property string path: bgRoot.wallpaperPath
|
||||
command: ["magick", "identify", "-format", "%w %h", path]
|
||||
stdout: StdioCollector {
|
||||
id: wallpaperSizeOutputCollector
|
||||
onStreamFinished: {
|
||||
const output = wallpaperSizeOutputCollector.text;
|
||||
const [width, height] = output.split(" ").map(Number);
|
||||
const [screenWidth, screenHeight] = [bgRoot.screen.width, bgRoot.screen.height];
|
||||
bgRoot.wallpaperWidth = width;
|
||||
bgRoot.wallpaperHeight = height;
|
||||
|
||||
if (width <= screenWidth || height <= screenHeight) {
|
||||
// Undersized/perfectly sized wallpapers
|
||||
bgRoot.effectiveWallpaperScale = Math.max(screenWidth / width, screenHeight / height);
|
||||
} else {
|
||||
// Oversized = can be zoomed for parallax, yay
|
||||
bgRoot.effectiveWallpaperScale = Math.min(bgRoot.preferredWallpaperScale, width / screenWidth, height / screenHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
// Wallpaper
|
||||
StyledImage {
|
||||
id: wallpaper
|
||||
visible: opacity > 0 && !blurLoader.active
|
||||
opacity: (status === Image.Ready && !bgRoot.wallpaperIsVideo) ? 1 : 0
|
||||
cache: false
|
||||
smooth: false
|
||||
// Range = groups that workspaces span on
|
||||
property int chunkSize: Config?.options.bar.workspaces.shown ?? 10
|
||||
property int lower: Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize
|
||||
property int upper: Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize
|
||||
property int range: upper - lower
|
||||
property real valueX: {
|
||||
let result = 0.5;
|
||||
if (Config.options.background.parallax.enableWorkspace && !bgRoot.verticalParallax) {
|
||||
result = ((bgRoot.monitor.activeWorkspace?.id - lower) / range);
|
||||
}
|
||||
if (Config.options.background.parallax.enableSidebar) {
|
||||
result += (0.15 * GlobalStates.sidebarRightOpen - 0.15 * GlobalStates.sidebarLeftOpen);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
property real valueY: {
|
||||
let result = 0.5;
|
||||
if (Config.options.background.parallax.enableWorkspace && bgRoot.verticalParallax) {
|
||||
result = ((bgRoot.monitor.activeWorkspace?.id - lower) / range);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
property real effectiveValueX: Math.max(0, Math.min(1, valueX))
|
||||
property real effectiveValueY: Math.max(0, Math.min(1, valueY))
|
||||
x: -(bgRoot.movableXSpace) - (effectiveValueX - 0.5) * 2 * bgRoot.movableXSpace
|
||||
y: -(bgRoot.movableYSpace) - (effectiveValueY - 0.5) * 2 * bgRoot.movableYSpace
|
||||
source: bgRoot.wallpaperSafetyTriggered ? "" : bgRoot.wallpaperPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
sourceSize {
|
||||
width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale * bgRoot.monitor.scale
|
||||
height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale * bgRoot.monitor.scale
|
||||
}
|
||||
width: bgRoot.wallpaperWidth / bgRoot.wallpaperToScreenRatio * bgRoot.effectiveWallpaperScale
|
||||
height: bgRoot.wallpaperHeight / bgRoot.wallpaperToScreenRatio * bgRoot.effectiveWallpaperScale
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: blurLoader
|
||||
active: Config.options.lock.blur.enable && (GlobalStates.screenLocked || scaleAnim.running)
|
||||
anchors.fill: wallpaper
|
||||
scale: GlobalStates.screenLocked ? Config.options.lock.blur.extraZoom : 1
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
id: scaleAnim
|
||||
duration: 400
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
sourceComponent: GaussianBlur {
|
||||
source: wallpaper
|
||||
radius: GlobalStates.screenLocked ? Config.options.lock.blur.radius : 0
|
||||
samples: radius * 2 + 1
|
||||
|
||||
Rectangle {
|
||||
opacity: GlobalStates.screenLocked ? 1 : 0
|
||||
anchors.fill: parent
|
||||
color: CF.ColorUtils.transparentize(Appearance.colors.colLayer0, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WidgetCanvas {
|
||||
id: widgetCanvas
|
||||
anchors {
|
||||
left: wallpaper.left
|
||||
right: wallpaper.right
|
||||
top: wallpaper.top
|
||||
bottom: wallpaper.bottom
|
||||
horizontalCenter: undefined
|
||||
verticalCenter: undefined
|
||||
readonly property real parallaxFactor: Config.options.background.parallax.widgetsFactor
|
||||
leftMargin: {
|
||||
const xOnWallpaper = bgRoot.movableXSpace;
|
||||
const extraMove = (wallpaper.effectiveValueX * 2 * bgRoot.movableXSpace) * (parallaxFactor - 1);
|
||||
return xOnWallpaper - extraMove;
|
||||
}
|
||||
topMargin: {
|
||||
const yOnWallpaper = bgRoot.movableYSpace;
|
||||
const extraMove = (wallpaper.effectiveValueY * 2 * bgRoot.movableYSpace) * (parallaxFactor - 1);
|
||||
return yOnWallpaper - extraMove;
|
||||
}
|
||||
Behavior on leftMargin {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on topMargin {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
width: wallpaper.width
|
||||
height: wallpaper.height
|
||||
states: State {
|
||||
name: "centered"
|
||||
when: GlobalStates.screenLocked || bgRoot.wallpaperSafetyTriggered
|
||||
PropertyChanges {
|
||||
target: widgetCanvas
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
}
|
||||
AnchorChanges {
|
||||
target: widgetCanvas
|
||||
anchors {
|
||||
left: undefined
|
||||
right: undefined
|
||||
top: undefined
|
||||
bottom: undefined
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
PropertyAnimation {
|
||||
properties: "width,height"
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
AnchorAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
FadeLoader {
|
||||
shown: Config.options.background.widgets.weather.enable
|
||||
sourceComponent: WeatherWidget {
|
||||
screenWidth: bgRoot.screen.width
|
||||
screenHeight: bgRoot.screen.height
|
||||
scaledScreenWidth: bgRoot.screen.width / bgRoot.effectiveWallpaperScale
|
||||
scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale
|
||||
wallpaperScale: bgRoot.effectiveWallpaperScale
|
||||
}
|
||||
}
|
||||
|
||||
FadeLoader {
|
||||
shown: Config.options.background.widgets.clock.enable
|
||||
sourceComponent: ClockWidget {
|
||||
screenWidth: bgRoot.screen.width
|
||||
screenHeight: bgRoot.screen.height
|
||||
scaledScreenWidth: bgRoot.screen.width / bgRoot.effectiveWallpaperScale
|
||||
scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale
|
||||
wallpaperScale: bgRoot.effectiveWallpaperScale
|
||||
wallpaperSafetyTriggered: bgRoot.wallpaperSafetyTriggered
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets.widgetCanvas
|
||||
|
||||
AbstractWidget {
|
||||
id: root
|
||||
|
||||
required property string configEntryName
|
||||
required property int screenWidth
|
||||
required property int screenHeight
|
||||
required property int scaledScreenWidth
|
||||
required property int scaledScreenHeight
|
||||
required property real wallpaperScale
|
||||
property bool visibleWhenLocked: false
|
||||
property var configEntry: Config.options.background.widgets[configEntryName]
|
||||
property string placementStrategy: configEntry.placementStrategy
|
||||
property real targetX: Math.max(0, Math.min(configEntry.x, scaledScreenWidth - width))
|
||||
property real targetY : Math.max(0, Math.min(configEntry.y, scaledScreenHeight - height))
|
||||
x: targetX
|
||||
y: targetY
|
||||
visible: opacity > 0
|
||||
opacity: (GlobalStates.screenLocked && !visibleWhenLocked) ? 0 : 1
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
scale: (draggable && containsPress) ? 1.05 : 1
|
||||
Behavior on scale {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
draggable: placementStrategy === "free"
|
||||
onReleased: {
|
||||
root.targetX = root.x;
|
||||
root.targetY = root.y;
|
||||
configEntry.x = root.targetX;
|
||||
configEntry.y = root.targetY;
|
||||
}
|
||||
|
||||
property bool needsColText: false
|
||||
property color dominantColor: Appearance.colors.colPrimary
|
||||
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
|
||||
property color colText: {
|
||||
const onNormalBackground = (GlobalStates.screenLocked && Config.options.lock.blur.enable)
|
||||
const adaptiveColor = ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12))
|
||||
return onNormalBackground ? Appearance.colors.colOnLayer0 : adaptiveColor;
|
||||
}
|
||||
|
||||
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov")
|
||||
property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath
|
||||
|
||||
onWallpaperPathChanged: refreshPlacementIfNeeded()
|
||||
onPlacementStrategyChanged: refreshPlacementIfNeeded()
|
||||
Connections {
|
||||
target: Config
|
||||
function onReadyChanged() { refreshPlacementIfNeeded() }
|
||||
}
|
||||
function refreshPlacementIfNeeded() {
|
||||
if (!Config.ready || (root.placementStrategy === "free" && root.needsColText)) return;
|
||||
leastBusyRegionProc.wallpaperPath = root.wallpaperPath;
|
||||
leastBusyRegionProc.running = false;
|
||||
leastBusyRegionProc.running = true;
|
||||
}
|
||||
Process {
|
||||
id: leastBusyRegionProc
|
||||
property string wallpaperPath: root.wallpaperPath
|
||||
// TODO: make these less arbitrary
|
||||
property int contentWidth: 300
|
||||
property int contentHeight: 300
|
||||
property int horizontalPadding: 200
|
||||
property int verticalPadding: 200
|
||||
command: [Quickshell.shellPath("scripts/images/least-busy-region-venv.sh") // Comments to force the formatter to break lines
|
||||
, "--screen-width", Math.round(root.scaledScreenWidth) //
|
||||
, "--screen-height", Math.round(root.scaledScreenHeight) //
|
||||
, "--width", contentWidth //
|
||||
, "--height", contentHeight //
|
||||
, "--horizontal-padding", horizontalPadding //
|
||||
, "--vertical-padding", verticalPadding //
|
||||
, wallpaperPath //
|
||||
, ...(root.placementStrategy === "mostBusy" ? ["--busiest"] : [])
|
||||
// "--visual-output",
|
||||
]
|
||||
stdout: StdioCollector {
|
||||
id: leastBusyRegionOutputCollector
|
||||
onStreamFinished: {
|
||||
const output = leastBusyRegionOutputCollector.text;
|
||||
// console.log("[Background] Least busy region output:", output)
|
||||
if (output.length === 0) return;
|
||||
const parsedContent = JSON.parse(output);
|
||||
root.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary;
|
||||
if (root.placementStrategy === "free") return;
|
||||
root.targetX = parsedContent.center_x * root.wallpaperScale - root.width / 2;
|
||||
root.targetY = parsedContent.center_y * root.wallpaperScale - root.height / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.widgets.widgetCanvas
|
||||
import qs.modules.ii.background.widgets
|
||||
|
||||
AbstractBackgroundWidget {
|
||||
id: root
|
||||
|
||||
configEntryName: "clock"
|
||||
|
||||
implicitHeight: contentColumn.implicitHeight
|
||||
implicitWidth: contentColumn.implicitWidth
|
||||
|
||||
readonly property string clockStyle: Config.options.background.widgets.clock.style
|
||||
readonly property bool forceCenter: (GlobalStates.screenLocked && Config.options.lock.centerClock)
|
||||
readonly property bool shouldShow: (!Config.options.background.widgets.clock.showOnlyWhenLocked || GlobalStates.screenLocked)
|
||||
property bool wallpaperSafetyTriggered: false
|
||||
needsColText: clockStyle === "digital"
|
||||
x: forceCenter ? ((root.screenWidth - root.width) / 2) : targetX
|
||||
y: forceCenter ? ((root.screenHeight - root.height) / 2) : targetY
|
||||
visibleWhenLocked: true
|
||||
|
||||
property var textHorizontalAlignment: {
|
||||
if (root.forceCenter)
|
||||
return Text.AlignHCenter;
|
||||
if (root.x < root.scaledScreenWidth / 3)
|
||||
return Text.AlignLeft;
|
||||
if (root.x > root.scaledScreenWidth * 2 / 3)
|
||||
return Text.AlignRight;
|
||||
return Text.AlignHCenter;
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
FadeLoader {
|
||||
id: cookieClockLoader
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
shown: root.clockStyle === "cookie" && (root.shouldShow)
|
||||
sourceComponent: Column {
|
||||
spacing: 10
|
||||
CookieClock {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
FadeLoader {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
shown: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text !== ""
|
||||
sourceComponent: CookieQuote {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FadeLoader {
|
||||
id: digitalClockLoader
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
shown: root.clockStyle === "digital" && (root.shouldShow)
|
||||
sourceComponent: ColumnLayout {
|
||||
id: clockColumn
|
||||
spacing: 6
|
||||
|
||||
ClockText {
|
||||
font.pixelSize: 90
|
||||
text: DateTime.time
|
||||
}
|
||||
ClockText {
|
||||
Layout.topMargin: -5
|
||||
text: DateTime.longDate
|
||||
}
|
||||
StyledText {
|
||||
// Somehow gets fucked up if made a ClockText???
|
||||
visible: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text.length > 0
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: root.textHorizontalAlignment
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.normal
|
||||
weight: 350
|
||||
}
|
||||
color: root.colText
|
||||
style: Text.Raised
|
||||
styleColor: Appearance.colors.colShadow
|
||||
text: Config.options.background.widgets.clock.quote.text
|
||||
}
|
||||
}
|
||||
}
|
||||
StatusRow {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
component StatusRow: Item {
|
||||
id: statusText
|
||||
implicitHeight: statusTextBg.implicitHeight
|
||||
implicitWidth: statusTextBg.implicitWidth
|
||||
StyledRectangularShadow {
|
||||
target: statusTextBg
|
||||
visible: statusTextBg.visible && root.clockStyle === "cookie"
|
||||
opacity: statusTextBg.opacity
|
||||
}
|
||||
Rectangle {
|
||||
id: statusTextBg
|
||||
anchors.centerIn: parent
|
||||
clip: true
|
||||
opacity: (safetyStatusText.shown || lockStatusText.shown) ? 1 : 0
|
||||
visible: opacity > 0
|
||||
implicitHeight: statusTextRow.implicitHeight + 5 * 2
|
||||
implicitWidth: statusTextRow.implicitWidth + 5 * 2
|
||||
radius: Appearance.rounding.small
|
||||
color: ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, root.clockStyle === "cookie" ? 0 : 1)
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: statusTextRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 14
|
||||
Item {
|
||||
Layout.fillWidth: root.textHorizontalAlignment !== Text.AlignLeft
|
||||
implicitWidth: 1
|
||||
}
|
||||
ClockStatusText {
|
||||
id: safetyStatusText
|
||||
shown: root.wallpaperSafetyTriggered
|
||||
statusIcon: "hide_image"
|
||||
statusText: Translation.tr("Wallpaper safety enforced")
|
||||
}
|
||||
ClockStatusText {
|
||||
id: lockStatusText
|
||||
shown: GlobalStates.screenLocked && Config.options.lock.showLockedText
|
||||
statusIcon: "lock"
|
||||
statusText: Translation.tr("Locked")
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: root.textHorizontalAlignment !== Text.AlignRight
|
||||
implicitWidth: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ClockText: StyledText {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: root.textHorizontalAlignment
|
||||
font {
|
||||
family: Appearance.font.family.expressive
|
||||
pixelSize: 20
|
||||
weight: Font.DemiBold
|
||||
}
|
||||
color: root.colText
|
||||
style: Text.Raised
|
||||
styleColor: Appearance.colors.colShadow
|
||||
animateChange: Config.options.background.widgets.clock.digital.animateChange
|
||||
}
|
||||
component ClockStatusText: Row {
|
||||
id: statusTextRow
|
||||
property alias statusIcon: statusIconWidget.text
|
||||
property alias statusText: statusTextWidget.text
|
||||
property bool shown: true
|
||||
property color textColor: root.clockStyle === "cookie" ? Appearance.colors.colOnSecondaryContainer : root.colText
|
||||
opacity: shown ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
spacing: 4
|
||||
MaterialSymbol {
|
||||
id: statusIconWidget
|
||||
anchors.verticalCenter: statusTextRow.verticalCenter
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
color: statusTextRow.textColor
|
||||
style: Text.Raised
|
||||
styleColor: Appearance.colors.colShadow
|
||||
}
|
||||
ClockText {
|
||||
id: statusTextWidget
|
||||
color: statusTextRow.textColor
|
||||
anchors.verticalCenter: statusTextRow.verticalCenter
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.large
|
||||
weight: Font.Normal
|
||||
}
|
||||
style: Text.Raised
|
||||
styleColor: Appearance.colors.colShadow
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
|
||||
import qs.modules.ii.background.widgets.clock.dateIndicator
|
||||
import qs.modules.ii.background.widgets.clock.minuteMarks
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property string clockStyle: Config.options.background.widgets.clock.style
|
||||
|
||||
property real implicitSize: 230
|
||||
|
||||
property color colShadow: Appearance.colors.colShadow
|
||||
property color colBackground: Appearance.colors.colPrimaryContainer
|
||||
property color colOnBackground: ColorUtils.mix(Appearance.colors.colSecondary, Appearance.colors.colPrimaryContainer, 0.15)
|
||||
property color colBackgroundInfo: ColorUtils.mix(Appearance.colors.colPrimary, Appearance.colors.colPrimaryContainer, 0.55)
|
||||
property color colHourHand: Appearance.colors.colPrimary
|
||||
property color colMinuteHand: Appearance.colors.colTertiary
|
||||
property color colSecondHand: Appearance.colors.colPrimary
|
||||
|
||||
readonly property list<string> clockNumbers: DateTime.time.split(/[: ]/)
|
||||
readonly property int clockHour: parseInt(clockNumbers[0]) % 12
|
||||
readonly property int clockMinute: DateTime.clock.minutes
|
||||
readonly property int clockSecond: DateTime.clock.seconds
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
function applyStyle(sides, dialStyle, hourHandStyle, minuteHandStyle, secondHandStyle, dateStyle) {
|
||||
Config.options.background.widgets.clock.cookie.sides = sides
|
||||
Config.options.background.widgets.clock.cookie.dialNumberStyle = dialStyle
|
||||
Config.options.background.widgets.clock.cookie.hourHandStyle = hourHandStyle
|
||||
Config.options.background.widgets.clock.cookie.minuteHandStyle = minuteHandStyle
|
||||
Config.options.background.widgets.clock.cookie.secondHandStyle = secondHandStyle
|
||||
Config.options.background.widgets.clock.cookie.dateStyle = dateStyle
|
||||
}
|
||||
|
||||
function setClockPreset(category) {
|
||||
if (!Config.options.background.widgets.clock.cookie.aiStyling) return;
|
||||
if (category === "") return;
|
||||
print("[Cookie clock] Setting clock preset for category: " + category)
|
||||
// "abstract", "anime", "city", "minimalist", "landscape", "plants", "person", "space"
|
||||
if (category == "abstract") {
|
||||
applyStyle(9, "none", "fill", "medium", "dot", "bubble")
|
||||
} else if (category == "anime") {
|
||||
applyStyle(7, "none", "fill", "bold", "dot", "bubble")
|
||||
} else if (category == "city" || category == "space") {
|
||||
applyStyle(23, "full", "hollow", "thin", "classic", "bubble")
|
||||
} else if (category == "minimalist") {
|
||||
applyStyle(6, "none", "fill", "bold", "dot", "hide")
|
||||
} else if (category == "landscape") {
|
||||
applyStyle(14, "full", "hollow", "medium", "classic", "bubble")
|
||||
} else if (category == "plants") {
|
||||
applyStyle(9, "dots", "fill", "bold", "dot", "border")
|
||||
} else if (category == "person") {
|
||||
applyStyle(14, "full", "classic", "classic", "classic", "rect")
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Config
|
||||
function onReadyChanged() {
|
||||
categoryFileView.path = Directories.generatedWallpaperCategoryPath
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: categoryFileView
|
||||
path: ""
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
root.setClockPreset(categoryFileView.text().trim())
|
||||
}
|
||||
}
|
||||
|
||||
property bool useSineCookie: Config.options.background.widgets.clock.cookie.useSineCookie
|
||||
StyledDropShadow {
|
||||
target: useSineCookie ? sineCookieLoader : roundedPolygonCookieLoader
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: Config.options.background.widgets.clock.cookie.constantlyRotate
|
||||
duration: 30000
|
||||
easing.type: Easing.Linear
|
||||
loops: Animation.Infinite
|
||||
from: 360
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: sineCookieLoader
|
||||
z: 0
|
||||
visible: false // The DropShadow already draws it
|
||||
active: useSineCookie
|
||||
sourceComponent: SineCookie {
|
||||
implicitSize: root.implicitSize
|
||||
sides: Config.options.background.widgets.clock.cookie.sides
|
||||
color: root.colBackground
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
id: roundedPolygonCookieLoader
|
||||
z: 0
|
||||
visible: false // The DropShadow already draws it
|
||||
active: !useSineCookie
|
||||
sourceComponent: MaterialCookie {
|
||||
implicitSize: root.implicitSize
|
||||
sides: Config.options.background.widgets.clock.cookie.sides
|
||||
color: root.colBackground
|
||||
}
|
||||
}
|
||||
|
||||
// Hour/minutes numbers/dots/lines
|
||||
MinuteMarks {
|
||||
anchors.fill: parent
|
||||
color: root.colOnBackground
|
||||
}
|
||||
|
||||
// Stupid extra hour marks in the middle
|
||||
FadeLoader {
|
||||
id: hourMarksLoader
|
||||
anchors.centerIn: parent
|
||||
shown: Config.options.background.widgets.clock.cookie.hourMarks
|
||||
sourceComponent: HourMarks {
|
||||
implicitSize: 135 * (1.75 - 0.75 * hourMarksLoader.opacity)
|
||||
color: root.colOnBackground
|
||||
colOnBackground: ColorUtils.mix(root.colBackgroundInfo, root.colOnBackground, 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// Number column in the middle
|
||||
FadeLoader {
|
||||
id: timeColumnLoader
|
||||
anchors.centerIn: parent
|
||||
shown: Config.options.background.widgets.clock.cookie.timeIndicators
|
||||
scale: 1.4 - 0.4 * timeColumnLoader.shown
|
||||
Behavior on scale {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
sourceComponent: TimeColumn {
|
||||
color: root.colBackgroundInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Minute hand
|
||||
FadeLoader {
|
||||
anchors.fill: parent
|
||||
z: 1
|
||||
shown: Config.options.background.widgets.clock.cookie.minuteHandStyle !== "hide"
|
||||
sourceComponent: MinuteHand {
|
||||
anchors.fill: parent
|
||||
clockMinute: root.clockMinute
|
||||
style: Config.options.background.widgets.clock.cookie.minuteHandStyle
|
||||
color: root.colMinuteHand
|
||||
}
|
||||
}
|
||||
|
||||
// Hour hand
|
||||
FadeLoader {
|
||||
anchors.fill: parent
|
||||
z: item?.style === "hollow" ? 0 : 2
|
||||
shown: Config.options.background.widgets.clock.cookie.hourHandStyle !== "hide"
|
||||
sourceComponent: HourHand {
|
||||
clockHour: root.clockHour
|
||||
clockMinute: root.clockMinute
|
||||
style: Config.options.background.widgets.clock.cookie.hourHandStyle
|
||||
color: root.colHourHand
|
||||
}
|
||||
}
|
||||
|
||||
// Second hand
|
||||
FadeLoader {
|
||||
id: secondHandLoader
|
||||
z: (Config.options.background.widgets.clock.cookie.secondHandStyle === "line") ? 2 : 3
|
||||
shown: Config.options.time.secondPrecision && Config.options.background.widgets.clock.cookie.secondHandStyle !== "hide"
|
||||
anchors.fill: parent
|
||||
sourceComponent: SecondHand {
|
||||
id: secondHand
|
||||
clockSecond: root.clockSecond
|
||||
style: Config.options.background.widgets.clock.cookie.secondHandStyle
|
||||
color: root.colSecondHand
|
||||
}
|
||||
}
|
||||
|
||||
// Center dot
|
||||
FadeLoader {
|
||||
z: 4
|
||||
anchors.centerIn: parent
|
||||
shown: Config.options.background.widgets.clock.cookie.minuteHandStyle !== "bold"
|
||||
sourceComponent: Rectangle {
|
||||
color: Config.options.background.widgets.clock.cookie.minuteHandStyle === "medium" ? root.colBackground : root.colMinuteHand
|
||||
implicitWidth: 6
|
||||
implicitHeight: implicitWidth
|
||||
radius: width / 2
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
FadeLoader {
|
||||
anchors.fill: parent
|
||||
shown: Config.options.background.widgets.clock.cookie.dateStyle !== "hide"
|
||||
|
||||
sourceComponent: DateIndicator {
|
||||
color: root.colBackgroundInfo
|
||||
style: Config.options.background.widgets.clock.cookie.dateStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property string quoteText: Config.options.background.widgets.clock.quote.text
|
||||
|
||||
implicitWidth: quoteBox.implicitWidth
|
||||
implicitHeight: quoteBox.implicitHeight
|
||||
|
||||
DropShadow {
|
||||
source: quoteBox
|
||||
anchors.fill: quoteBox
|
||||
horizontalOffset: 0
|
||||
verticalOffset: 2
|
||||
radius: 12
|
||||
samples: radius * 2 + 1
|
||||
color: Appearance.colors.colShadow
|
||||
transparentBorder: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: quoteBox
|
||||
|
||||
implicitWidth: quoteRow.implicitWidth + 8 * 2
|
||||
implicitHeight: quoteRow.implicitHeight + 4 * 2
|
||||
radius: Appearance.rounding.small
|
||||
color: Appearance.colors.colSecondaryContainer
|
||||
|
||||
Row {
|
||||
id: quoteRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
MaterialSymbol {
|
||||
id: quoteIcon
|
||||
anchors.top: parent.top
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
text: "format_quote"
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
StyledText {
|
||||
id: quoteStyledText
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: Config.options.background.widgets.clock.quote.text
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
font {
|
||||
family: Appearance.font.family.reading
|
||||
pixelSize: Appearance.font.pixelSize.large
|
||||
weight: Font.Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property int clockHour
|
||||
required property int clockMinute
|
||||
property real handLength: 72
|
||||
property real handWidth: 20
|
||||
property string style: "fill"
|
||||
property color color: Appearance.colors.colPrimary
|
||||
|
||||
property real fillColorAlpha: root.style === "hollow" ? 0 : 1
|
||||
Behavior on fillColorAlpha {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
rotation: -90 + (360 / 12) * (root.clockHour + root.clockMinute / 60)
|
||||
Behavior on rotation {
|
||||
animation: RotationAnimation {
|
||||
direction: RotationAnimation.Clockwise
|
||||
duration: 300
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: (parent.width - root.handWidth) / 2 - 15 * (root.style === "classic")
|
||||
width: root.handLength
|
||||
height: root.style === "classic" ? 8 : root.handWidth
|
||||
radius: root.style === "classic" ? 2 : root.handWidth / 2
|
||||
color : Qt.rgba(root.color.r, root.color.g, root.color.b, root.fillColorAlpha)
|
||||
border.color: root.color
|
||||
border.width: 4
|
||||
|
||||
Behavior on x {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real implicitSize: 135
|
||||
property real markLength: 12
|
||||
property real markWidth: 4
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
property color colOnBackground: Appearance.colors.colSecondaryContainer
|
||||
property real padding: 8
|
||||
|
||||
Rectangle {
|
||||
color: root.color
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: root.implicitSize
|
||||
implicitHeight: root.implicitSize
|
||||
radius: width / 2
|
||||
|
||||
// Hour mark lines
|
||||
Repeater {
|
||||
model: 12
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
anchors.fill: parent
|
||||
rotation: 360 / 12 * index
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: root.padding
|
||||
}
|
||||
implicitWidth: root.markLength
|
||||
implicitHeight: root.markWidth
|
||||
|
||||
radius: width / 2
|
||||
color: root.colOnBackground
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
required property int clockMinute
|
||||
property string style: "medium"
|
||||
property real handLength: 95
|
||||
property real handWidth: style === "bold" ? 20 : style === "medium" ? 12 : 5
|
||||
property color color: Appearance.colors.colTertiary
|
||||
|
||||
rotation: -90 + (360 / 60) * root.clockMinute
|
||||
Behavior on rotation {
|
||||
animation: RotationAnimation {
|
||||
direction: RotationAnimation.Clockwise
|
||||
duration: 300
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: {
|
||||
let position = parent.width / 2 - root.handWidth / 2;
|
||||
if (root.style === "classic") position -= 15;
|
||||
return position;
|
||||
}
|
||||
width: root.handLength
|
||||
height: root.handWidth
|
||||
|
||||
radius: root.style === "classic" ? 2 : root.handWidth / 2
|
||||
color: root.color
|
||||
|
||||
Behavior on height {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
required property int clockSecond
|
||||
property real handWidth: 2
|
||||
property real handLength: 95
|
||||
property real dotSize: 20
|
||||
property string style: "hide"
|
||||
property color color: Appearance.colors.colSecondary
|
||||
|
||||
rotation: (360 / 60 * clockSecond) + 90
|
||||
|
||||
Behavior on rotation {
|
||||
enabled: Config.options.background.widgets.clock.cookie.constantlyRotate // Animating every second is expensive...
|
||||
animation: RotationAnimation {
|
||||
direction: RotationAnimation.Clockwise
|
||||
duration: 1000 // 1 second
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: 10 + (root.style === "dot" ? root.dotSize : 0)
|
||||
}
|
||||
implicitWidth: root.style === "dot" ? root.dotSize : root.handLength
|
||||
implicitHeight: root.style === "dot" ? root.dotSize : root.handWidth
|
||||
radius: Math.min(width, height) / 2
|
||||
color: root.color
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Classic style dot in the middle of the hand
|
||||
FadeLoader {
|
||||
id: classicDotLoader
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
shown: root.style === "classic"
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: 40
|
||||
}
|
||||
implicitWidth: root.style === "classic" ? 14 : 0
|
||||
implicitHeight: implicitWidth
|
||||
color: root.color
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
property list<string> clockNumbers: DateTime.time.split(/[: ]/)
|
||||
property bool isEnabled: Config.options.background.widgets.clock.cookie.timeIndicators
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
|
||||
property bool hourMarksEnabled: Config.options.background.widgets.clock.cookie.hourMarks
|
||||
spacing: -16
|
||||
|
||||
Repeater {
|
||||
model: root.clockNumbers
|
||||
|
||||
delegate: StyledText {
|
||||
required property string modelData
|
||||
text: modelData.padStart(2, "0")
|
||||
property bool isAmPm: !text.match(/\d{2}/i)
|
||||
property real numberSizeWithoutGlow: isAmPm ? 26 : 68
|
||||
property real numberSizeWithGlow: isAmPm ? 20 : 40
|
||||
property real numberSize: root.hourMarksEnabled ? numberSizeWithGlow : numberSizeWithoutGlow
|
||||
|
||||
anchors.horizontalCenter: root.horizontalCenter
|
||||
color: root.color
|
||||
font {
|
||||
family: Appearance.font.family.expressive
|
||||
weight: Font.Bold
|
||||
pixelSize: numberSize
|
||||
}
|
||||
|
||||
Behavior on numberSize {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool isMonth: false
|
||||
property real targetSize: 0
|
||||
property alias text: bubbleText.text
|
||||
|
||||
text: Qt.locale().toString(DateTime.clock.date, root.isMonth ? "MM" : "d")
|
||||
|
||||
MaterialShape {
|
||||
id: bubble
|
||||
z: 5
|
||||
// sides: root.isMonth ? 1 : 4
|
||||
shape: root.isMonth ? MaterialShape.Shape.Pill : MaterialShape.Shape.Pentagon
|
||||
anchors.centerIn: parent
|
||||
color: root.isMonth ? Appearance.colors.colSecondaryContainer : Appearance.colors.colTertiaryContainer
|
||||
implicitSize: targetSize
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: bubbleText
|
||||
z: 6
|
||||
anchors.centerIn: parent
|
||||
color: root.isMonth ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnTertiaryContainer
|
||||
font {
|
||||
family: Appearance.font.family.expressive
|
||||
pixelSize: 30
|
||||
weight: Font.Black
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property string style: "bubble"
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
property real dateSquareSize: 64
|
||||
|
||||
// Rotating date
|
||||
FadeLoader {
|
||||
anchors.fill: parent
|
||||
shown: Config.options.background.widgets.clock.cookie.dateStyle === "border"
|
||||
sourceComponent: RotatingDate {
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
|
||||
// Rectangle date (only today's number) in right side of the clock
|
||||
FadeLoader {
|
||||
id: rectLoader
|
||||
shown: root.style === "rect"
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
right: parent.right
|
||||
rightMargin: 40 - rectLoader.opacity * 30
|
||||
}
|
||||
|
||||
sourceComponent: RectangleDate {
|
||||
color: ColorUtils.mix(root.color, Appearance.colors.colSecondaryContainerHover, 0.5)
|
||||
radius: Appearance.rounding.small
|
||||
implicitWidth: 45 * rectLoader.opacity
|
||||
implicitHeight: 30 * rectLoader.opacity
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble style: day of month
|
||||
FadeLoader {
|
||||
id: dayBubbleLoader
|
||||
shown: root.style === "bubble"
|
||||
property real targetSize: root.dateSquareSize * opacity
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
sourceComponent: BubbleDate {
|
||||
implicitWidth: dayBubbleLoader.targetSize
|
||||
implicitHeight: dayBubbleLoader.targetSize
|
||||
isMonth: false
|
||||
targetSize: dayBubbleLoader.targetSize
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble style: month
|
||||
FadeLoader {
|
||||
id: monthBubbleLoader
|
||||
shown: root.style === "bubble"
|
||||
property real targetSize: root.dateSquareSize * opacity
|
||||
anchors {
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
sourceComponent: BubbleDate {
|
||||
implicitWidth: monthBubbleLoader.targetSize
|
||||
implicitHeight: monthBubbleLoader.targetSize
|
||||
isMonth: true
|
||||
targetSize: monthBubbleLoader.targetSize
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: rect
|
||||
readonly property string dialStyle: Config.options.background.widgets.clock.cookie.dialNumberStyle
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colSecondaryHover
|
||||
text: Qt.locale().toString(DateTime.clock.date, "dd")
|
||||
font {
|
||||
family: Appearance.font.family.expressive
|
||||
pixelSize: 20
|
||||
weight: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string style: Config.options.background.widgets.clock.cookie.dateStyle
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
property real angleStep: 12 * Math.PI / 180
|
||||
property string dateText: Qt.locale().toString(DateTime.clock.date, "ddd dd")
|
||||
|
||||
readonly property int clockSecond: DateTime.clock.seconds
|
||||
readonly property string dialStyle: Config.options.background.widgets.clock.cookie.dialNumberStyle
|
||||
readonly property bool timeIndicators: Config.options.background.widgets.clock.cookie.timeIndicators
|
||||
|
||||
property real radius: style === "border" ? 90 : 0
|
||||
Behavior on radius {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
rotation: {
|
||||
if (!Config.options.time.secondPrecision) return 0
|
||||
else return (360 / 60 * clockSecond) + 180 - (angleStep / Math.PI * 180 * dateText.length) / 2
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.dateText.length
|
||||
|
||||
delegate: Text {
|
||||
required property int index
|
||||
property real angle: index * root.angleStep - Math.PI / 2
|
||||
x: root.width / 2 + root.radius * Math.cos(angle) - width / 2
|
||||
y: root.height / 2 + root.radius * Math.sin(angle) - height / 2
|
||||
rotation: angle * 180 / Math.PI + 90
|
||||
|
||||
color: root.color
|
||||
font {
|
||||
family: Appearance.font.family.title
|
||||
pixelSize: 30
|
||||
variableAxes: Appearance.font.variableAxes.title
|
||||
}
|
||||
|
||||
text: root.dateText.charAt(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real numberSize: 80
|
||||
property real margins: 10
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
|
||||
property int hours: 12
|
||||
property int numbers: 4
|
||||
property int fontSize: 80
|
||||
|
||||
Repeater {
|
||||
model: root.numbers
|
||||
|
||||
Item {
|
||||
id: numberItem
|
||||
required property int index
|
||||
rotation: 360 / root.numbers * (index + 1)
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
implicitWidth: root.numberSize
|
||||
implicitHeight: implicitWidth
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: root.margins
|
||||
}
|
||||
StyledText {
|
||||
color: root.color
|
||||
anchors.centerIn: parent
|
||||
text: root.hours / root.numbers * (numberItem.index + 1)
|
||||
rotation: -numberItem.rotation
|
||||
|
||||
font {
|
||||
family: Appearance.font.family.reading
|
||||
pixelSize: root.fontSize
|
||||
weight: Font.Black
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real implicitSize: 12
|
||||
property real margins: 10
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
|
||||
Repeater {
|
||||
model: 12
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
anchors.fill: parent // Ensures rotation works properly
|
||||
rotation: 360 / 12 * index
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: root.margins
|
||||
}
|
||||
implicitWidth: root.implicitSize
|
||||
implicitHeight: implicitWidth
|
||||
radius: implicitWidth / 2
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real numberSize: 80
|
||||
property real margins: 10
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
|
||||
property real hourLineSize: 4
|
||||
property real minuteLineSize: 2
|
||||
property real hourLineLength: 18
|
||||
property real minuteLineLength: 7
|
||||
|
||||
property int hours: 12
|
||||
property int minutes: 60
|
||||
|
||||
// Full dial style hour lines
|
||||
Repeater {
|
||||
model: root.hours
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
rotation: 360 / root.hours * index
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: root.margins
|
||||
}
|
||||
implicitWidth: root.hourLineLength
|
||||
implicitHeight: root.hourLineSize
|
||||
radius: implicitWidth / 2
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minute lines
|
||||
Repeater {
|
||||
model: root.minutes
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
rotation: 360 / root.minutes * index
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: root.margins
|
||||
}
|
||||
implicitWidth: root.minuteLineLength
|
||||
implicitHeight: root.minuteLineSize
|
||||
radius: implicitWidth / 2
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: Appearance.colors.colOnSecondaryContainer
|
||||
property string style: Config.options.background.widgets.clock.cookie.dialNumberStyle // "dots", "numbers", "full", "hide"
|
||||
property string dateStyle : Config.options.background.widgets.clock.cookie.dateStyle
|
||||
|
||||
// 12 Dots
|
||||
FadeLoader {
|
||||
id: dotsLoader
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 10
|
||||
}
|
||||
shown: root.style === "dots"
|
||||
sourceComponent: Dots {
|
||||
color: root.color
|
||||
margins: 46 - dotsLoader.opacity * 34
|
||||
}
|
||||
}
|
||||
|
||||
// 3-6-9-12 hour numbers (pls don't realize you can have more than 4 numbers)
|
||||
FadeLoader {
|
||||
id: bigHourNumbersLoader
|
||||
anchors.fill: parent
|
||||
shown: root.style === "numbers"
|
||||
sourceComponent: BigHourNumbers {
|
||||
numberSize: 80
|
||||
color: root.color
|
||||
margins: 20 - 10 * bigHourNumbersLoader.opacity
|
||||
}
|
||||
}
|
||||
|
||||
// Lines
|
||||
FadeLoader {
|
||||
id: linesLoader
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 10
|
||||
}
|
||||
shown: root.style === "full"
|
||||
sourceComponent: Lines {
|
||||
color: root.color
|
||||
margins: 46 - linesLoader.opacity * 34
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import QtQuick
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.widgets.widgetCanvas
|
||||
import qs.modules.ii.background.widgets
|
||||
|
||||
AbstractBackgroundWidget {
|
||||
id: root
|
||||
|
||||
configEntryName: "weather"
|
||||
|
||||
implicitHeight: backgroundShape.implicitHeight
|
||||
implicitWidth: backgroundShape.implicitWidth
|
||||
|
||||
StyledDropShadow {
|
||||
target: backgroundShape
|
||||
}
|
||||
|
||||
MaterialShape {
|
||||
id: backgroundShape
|
||||
anchors.fill: parent
|
||||
shape: MaterialShape.Shape.Pill
|
||||
color: Appearance.colors.colPrimaryContainer
|
||||
implicitSize: 200
|
||||
|
||||
StyledText {
|
||||
font {
|
||||
pixelSize: 80
|
||||
family: Appearance.font.family.expressive
|
||||
weight: Font.Medium
|
||||
}
|
||||
color: Appearance.colors.colPrimary
|
||||
text: Weather.data?.temp.substring(0,Weather.data?.temp.length - 1) ?? "--°"
|
||||
anchors {
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
rightMargin: 16
|
||||
topMargin: 20
|
||||
}
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
iconSize: 80
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
text: Icons.getWeatherIcon(Weather.data.wCode) ?? "cloud"
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
|
||||
leftMargin: 16
|
||||
bottomMargin: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
|
||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
||||
|
||||
property string activeWindowAddress: `0x${activeWindow?.HyprlandToplevel?.address}`
|
||||
property bool focusingThisMonitor: HyprlandData.activeWorkspace?.monitor == monitor?.name
|
||||
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor?.id]?.activeWorkspace.id)
|
||||
|
||||
implicitWidth: colLayout.implicitWidth
|
||||
|
||||
ColumnLayout {
|
||||
id: colLayout
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: -4
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: Appearance.colors.colSubtext
|
||||
elide: Text.ElideRight
|
||||
text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ?
|
||||
root.activeWindow?.appId :
|
||||
(root.biggestWindow?.class) ?? Translation.tr("Desktop")
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer0
|
||||
elide: Text.ElideRight
|
||||
text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ?
|
||||
root.activeWindow?.title :
|
||||
(root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${monitor?.activeWorkspace?.id ?? 1}`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Scope {
|
||||
id: bar
|
||||
property bool showBarBackground: Config.options.bar.showBackground
|
||||
|
||||
Variants {
|
||||
// For each monitor
|
||||
model: {
|
||||
const screens = Quickshell.screens;
|
||||
const list = Config.options.bar.screenList;
|
||||
if (!list || list.length === 0)
|
||||
return screens;
|
||||
return screens.filter(screen => list.includes(screen.name));
|
||||
}
|
||||
LazyLoader {
|
||||
id: barLoader
|
||||
active: GlobalStates.barOpen && !GlobalStates.screenLocked
|
||||
required property ShellScreen modelData
|
||||
component: PanelWindow { // Bar window
|
||||
id: barRoot
|
||||
screen: barLoader.modelData
|
||||
|
||||
Timer {
|
||||
id: showBarTimer
|
||||
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
barRoot.superShow = true
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onSuperDownChanged() {
|
||||
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
|
||||
if (GlobalStates.superDown) showBarTimer.restart();
|
||||
else {
|
||||
showBarTimer.stop();
|
||||
barRoot.superShow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
property bool superShow: false
|
||||
property bool mustShow: hoverRegion.containsMouse || superShow
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
exclusiveZone: (Config?.options.bar.autoHide.enable && (!mustShow || !Config?.options.bar.autoHide.pushWindows)) ? 0 :
|
||||
Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0)
|
||||
WlrLayershell.namespace: "quickshell:bar"
|
||||
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
|
||||
mask: Region {
|
||||
item: hoverMaskRegion
|
||||
}
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: !Config.options.bar.bottom
|
||||
bottom: Config.options.bar.bottom
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
margins {
|
||||
right: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * -1
|
||||
bottom: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * -1
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hoverRegion
|
||||
hoverEnabled: true
|
||||
anchors {
|
||||
fill: parent
|
||||
rightMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * 1
|
||||
bottomMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: hoverMaskRegion
|
||||
anchors {
|
||||
fill: barContent
|
||||
topMargin: -Config.options.bar.autoHide.hoverRegionWidth
|
||||
bottomMargin: -Config.options.bar.autoHide.hoverRegionWidth
|
||||
}
|
||||
}
|
||||
|
||||
BarContent {
|
||||
id: barContent
|
||||
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
anchors {
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
bottom: undefined
|
||||
topMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight : 0
|
||||
bottomMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * -1
|
||||
rightMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * -1
|
||||
}
|
||||
Behavior on anchors.topMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on anchors.bottomMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
states: State {
|
||||
name: "bottom"
|
||||
when: Config.options.bar.bottom
|
||||
AnchorChanges {
|
||||
target: barContent
|
||||
anchors {
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
top: undefined
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: barContent
|
||||
anchors.topMargin: 0
|
||||
anchors.bottomMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Round decorators
|
||||
Loader {
|
||||
id: roundDecorators
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: barContent.bottom
|
||||
bottom: undefined
|
||||
}
|
||||
height: Appearance.rounding.screenRounding
|
||||
active: showBarBackground && Config.options.bar.cornerStyle === 0 // Hug
|
||||
|
||||
states: State {
|
||||
name: "bottom"
|
||||
when: Config.options.bar.bottom
|
||||
AnchorChanges {
|
||||
target: roundDecorators
|
||||
anchors {
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
top: undefined
|
||||
bottom: barContent.top
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: Appearance.rounding.screenRounding
|
||||
RoundCorner {
|
||||
id: leftCorner
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
}
|
||||
|
||||
implicitSize: Appearance.rounding.screenRounding
|
||||
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
|
||||
|
||||
corner: RoundCorner.CornerEnum.TopLeft
|
||||
states: State {
|
||||
name: "bottom"
|
||||
when: Config.options.bar.bottom
|
||||
PropertyChanges {
|
||||
leftCorner.corner: RoundCorner.CornerEnum.BottomLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
RoundCorner {
|
||||
id: rightCorner
|
||||
anchors {
|
||||
right: parent.right
|
||||
top: !Config.options.bar.bottom ? parent.top : undefined
|
||||
bottom: Config.options.bar.bottom ? parent.bottom : undefined
|
||||
}
|
||||
implicitSize: Appearance.rounding.screenRounding
|
||||
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
|
||||
|
||||
corner: RoundCorner.CornerEnum.TopRight
|
||||
states: State {
|
||||
name: "bottom"
|
||||
when: Config.options.bar.bottom
|
||||
PropertyChanges {
|
||||
rightCorner.corner: RoundCorner.CornerEnum.BottomRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "bar"
|
||||
|
||||
function toggle(): void {
|
||||
GlobalStates.barOpen = !GlobalStates.barOpen
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
GlobalStates.barOpen = false
|
||||
}
|
||||
|
||||
function open(): void {
|
||||
GlobalStates.barOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "barToggle"
|
||||
description: "Toggles bar on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.barOpen = !GlobalStates.barOpen;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "barOpen"
|
||||
description: "Opens bar on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.barOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "barClose"
|
||||
description: "Closes bar on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.barOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import qs.modules.ii.bar.weather
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Item { // Bar content region
|
||||
id: root
|
||||
|
||||
property var screen: root.QsWindow.window?.screen
|
||||
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
|
||||
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen?.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen?.width) ? 1 : 0
|
||||
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
|
||||
|
||||
component VerticalBarSeparator: Rectangle {
|
||||
Layout.topMargin: Appearance.sizes.baseBarHeight / 3
|
||||
Layout.bottomMargin: Appearance.sizes.baseBarHeight / 3
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 1
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
}
|
||||
|
||||
// Background shadow
|
||||
Loader {
|
||||
active: Config.options.bar.showBackground && Config.options.bar.cornerStyle === 1 && Config.options.bar.floatStyleShadow
|
||||
anchors.fill: barBackground
|
||||
sourceComponent: StyledRectangularShadow {
|
||||
anchors.fill: undefined // The loader's anchors act on this, and this should not have any anchor
|
||||
target: barBackground
|
||||
}
|
||||
}
|
||||
// Background
|
||||
Rectangle {
|
||||
id: barBackground
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed
|
||||
}
|
||||
color: Config.options.bar.showBackground ? Appearance.colors.colLayer0 : "transparent"
|
||||
radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0
|
||||
border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
}
|
||||
|
||||
FocusedScrollMouseArea { // Left side | scroll to change brightness
|
||||
id: barLeftSideMouseArea
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: middleSection.left
|
||||
}
|
||||
implicitWidth: leftSectionRowLayout.implicitWidth
|
||||
implicitHeight: Appearance.sizes.baseBarHeight
|
||||
|
||||
onScrollDown: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness - 0.05)
|
||||
onScrollUp: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness + 0.05)
|
||||
onMovedAway: GlobalStates.osdBrightnessOpen = false
|
||||
onPressed: event => {
|
||||
if (event.button === Qt.LeftButton)
|
||||
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
|
||||
}
|
||||
|
||||
// Visual content
|
||||
ScrollHint {
|
||||
reveal: barLeftSideMouseArea.hovered
|
||||
icon: "light_mode"
|
||||
tooltipText: Translation.tr("Scroll to change brightness")
|
||||
side: "left"
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: leftSectionRowLayout
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
LeftSidebarButton { // Left sidebar button
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Appearance.rounding.screenRounding
|
||||
colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
|
||||
}
|
||||
|
||||
ActiveWindow {
|
||||
visible: root.useShortenedForm === 0
|
||||
Layout.rightMargin: Appearance.rounding.screenRounding
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row { // Middle section
|
||||
id: middleSection
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
spacing: 4
|
||||
|
||||
BarGroup {
|
||||
id: leftCenterGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitWidth: root.centerSideModuleWidth
|
||||
|
||||
Resources {
|
||||
alwaysShowAllResources: root.useShortenedForm === 2
|
||||
Layout.fillWidth: root.useShortenedForm === 2
|
||||
}
|
||||
|
||||
Media {
|
||||
visible: root.useShortenedForm < 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
VerticalBarSeparator {
|
||||
visible: Config.options?.bar.borderless
|
||||
}
|
||||
|
||||
BarGroup {
|
||||
id: middleCenterGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
padding: workspacesWidget.widgetPadding
|
||||
|
||||
Workspaces {
|
||||
id: workspacesWidget
|
||||
Layout.fillHeight: true
|
||||
MouseArea {
|
||||
// Right-click to toggle overview
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onPressed: event => {
|
||||
if (event.button === Qt.RightButton) {
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalBarSeparator {
|
||||
visible: Config.options?.bar.borderless
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rightCenterGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitWidth: root.centerSideModuleWidth
|
||||
implicitHeight: rightCenterGroupContent.implicitHeight
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
|
||||
}
|
||||
|
||||
BarGroup {
|
||||
id: rightCenterGroupContent
|
||||
anchors.fill: parent
|
||||
|
||||
ClockWidget {
|
||||
showDate: (Config.options.bar.verbose && root.useShortenedForm < 2)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
UtilButtons {
|
||||
visible: (Config.options.bar.verbose && root.useShortenedForm === 0)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
BatteryIndicator {
|
||||
visible: (root.useShortenedForm < 2 && Battery.available)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusedScrollMouseArea { // Right side | scroll to change volume
|
||||
id: barRightSideMouseArea
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: middleSection.right
|
||||
right: parent.right
|
||||
}
|
||||
implicitWidth: rightSectionRowLayout.implicitWidth
|
||||
implicitHeight: Appearance.sizes.baseBarHeight
|
||||
|
||||
onScrollDown: {
|
||||
const currentVolume = Audio.value;
|
||||
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
|
||||
Audio.sink.audio.volume -= step;
|
||||
}
|
||||
onScrollUp: {
|
||||
const currentVolume = Audio.value;
|
||||
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
|
||||
Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
|
||||
}
|
||||
onMovedAway: GlobalStates.osdVolumeOpen = false;
|
||||
onPressed: event => {
|
||||
if (event.button === Qt.LeftButton) {
|
||||
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
|
||||
}
|
||||
}
|
||||
|
||||
// Visual content
|
||||
ScrollHint {
|
||||
reveal: barRightSideMouseArea.hovered
|
||||
icon: "volume_up"
|
||||
tooltipText: Translation.tr("Scroll to change volume")
|
||||
side: "right"
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rightSectionRowLayout
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
layoutDirection: Qt.RightToLeft
|
||||
|
||||
RippleButton { // Right sidebar button
|
||||
id: rightSidebarButton
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
Layout.rightMargin: Appearance.rounding.screenRounding
|
||||
Layout.fillWidth: false
|
||||
|
||||
implicitWidth: indicatorsRowLayout.implicitWidth + 10 * 2
|
||||
implicitHeight: indicatorsRowLayout.implicitHeight + 5 * 2
|
||||
|
||||
buttonRadius: Appearance.rounding.full
|
||||
colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
|
||||
colBackgroundHover: Appearance.colors.colLayer1Hover
|
||||
colRipple: Appearance.colors.colLayer1Active
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
toggled: GlobalStates.sidebarRightOpen
|
||||
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
|
||||
|
||||
Behavior on colText {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: indicatorsRowLayout
|
||||
anchors.centerIn: parent
|
||||
property real realSpacing: 15
|
||||
spacing: 0
|
||||
|
||||
Revealer {
|
||||
reveal: Audio.sink?.audio?.muted ?? false
|
||||
Layout.fillHeight: true
|
||||
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
|
||||
Behavior on Layout.rightMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
MaterialSymbol {
|
||||
text: "volume_off"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
}
|
||||
Revealer {
|
||||
reveal: Audio.source?.audio?.muted ?? false
|
||||
Layout.fillHeight: true
|
||||
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
|
||||
Behavior on Layout.rightMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
MaterialSymbol {
|
||||
text: "mic_off"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
}
|
||||
HyprlandXkbIndicator {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: indicatorsRowLayout.realSpacing
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
Revealer {
|
||||
reveal: Notifications.silent || Notifications.unread > 0
|
||||
Layout.fillHeight: true
|
||||
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
|
||||
implicitHeight: reveal ? notificationUnreadCount.implicitHeight : 0
|
||||
implicitWidth: reveal ? notificationUnreadCount.implicitWidth : 0
|
||||
Behavior on Layout.rightMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
NotificationUnreadCount {
|
||||
id: notificationUnreadCount
|
||||
}
|
||||
}
|
||||
MaterialSymbol {
|
||||
Layout.rightMargin: indicatorsRowLayout.realSpacing
|
||||
text: Network.materialSymbol
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
MaterialSymbol {
|
||||
visible: BluetoothStatus.available
|
||||
text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SysTray {
|
||||
visible: root.useShortenedForm === 0
|
||||
Layout.fillWidth: false
|
||||
Layout.fillHeight: true
|
||||
invertSide: Config?.options.bar.bottom
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
// Weather
|
||||
Loader {
|
||||
Layout.leftMargin: 4
|
||||
active: Config.options.bar.weather.enable
|
||||
|
||||
sourceComponent: BarGroup {
|
||||
WeatherBar {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool vertical: false
|
||||
property real padding: 5
|
||||
implicitWidth: vertical ? Appearance.sizes.baseVerticalBarWidth : (gridLayout.implicitWidth + padding * 2)
|
||||
implicitHeight: vertical ? (gridLayout.implicitHeight + padding * 2) : Appearance.sizes.baseBarHeight
|
||||
default property alias items: gridLayout.children
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: root.vertical ? 0 : 4
|
||||
bottomMargin: root.vertical ? 0 : 4
|
||||
leftMargin: root.vertical ? 4 : 0
|
||||
rightMargin: root.vertical ? 4 : 0
|
||||
}
|
||||
color: Config.options?.bar.borderless ? "transparent" : Appearance.colors.colLayer1
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: gridLayout
|
||||
columns: root.vertical ? 1 : -1
|
||||
anchors {
|
||||
verticalCenter: root.vertical ? undefined : parent.verticalCenter
|
||||
horizontalCenter: root.vertical ? parent.horizontalCenter : undefined
|
||||
left: root.vertical ? undefined : parent.left
|
||||
right: root.vertical ? undefined : parent.right
|
||||
top: root.vertical ? parent.top : undefined
|
||||
bottom: root.vertical ? parent.bottom : undefined
|
||||
margins: root.padding
|
||||
}
|
||||
columnSpacing: 4
|
||||
rowSpacing: 12
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
property bool borderless: Config.options.bar.borderless
|
||||
readonly property var chargeState: Battery.chargeState
|
||||
readonly property bool isCharging: Battery.isCharging
|
||||
readonly property bool isPluggedIn: Battery.isPluggedIn
|
||||
readonly property real percentage: Battery.percentage
|
||||
readonly property bool isLow: percentage <= Config.options.battery.low / 100
|
||||
|
||||
implicitWidth: batteryProgress.implicitWidth
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
ClippedProgressBar {
|
||||
id: batteryProgress
|
||||
anchors.centerIn: parent
|
||||
value: percentage
|
||||
highlightColor: (isLow && !isCharging) ? Appearance.m3colors.m3error : Appearance.colors.colOnSecondaryContainer
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: batteryProgress.valueBarWidth
|
||||
height: batteryProgress.valueBarHeight
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
MaterialSymbol {
|
||||
id: boltIcon
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: -2
|
||||
Layout.rightMargin: -2
|
||||
fill: 1
|
||||
text: "bolt"
|
||||
iconSize: Appearance.font.pixelSize.smaller
|
||||
visible: isCharging && percentage < 1 // TODO: animation
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
font: batteryProgress.font
|
||||
text: batteryProgress.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BatteryPopup {
|
||||
id: batteryPopup
|
||||
hoverTarget: root
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledPopup {
|
||||
id: root
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
// Header
|
||||
StyledPopupHeaderRow {
|
||||
icon: "battery_android_full"
|
||||
label: Translation.tr("Battery")
|
||||
}
|
||||
|
||||
StyledPopupValueRow {
|
||||
visible: {
|
||||
let timeValue = Battery.isCharging ? Battery.timeToFull : Battery.timeToEmpty;
|
||||
let power = Battery.energyRate;
|
||||
return !(Battery.chargeState == 4 || timeValue <= 0 || power <= 0.01);
|
||||
}
|
||||
icon: "schedule"
|
||||
label: Battery.isCharging ? Translation.tr("Time to full:") : Translation.tr("Time to empty:")
|
||||
value: {
|
||||
function formatTime(seconds) {
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor((seconds % 3600) / 60);
|
||||
if (h > 0)
|
||||
return `${h}h, ${m}m`;
|
||||
else
|
||||
return `${m}m`;
|
||||
}
|
||||
if (Battery.isCharging)
|
||||
return formatTime(Battery.timeToFull);
|
||||
else
|
||||
return formatTime(Battery.timeToEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
StyledPopupValueRow {
|
||||
visible: !(Battery.chargeState != 4 && Battery.energyRate == 0)
|
||||
icon: "bolt"
|
||||
label: {
|
||||
if (Battery.chargeState == 4) {
|
||||
return Translation.tr("Fully charged");
|
||||
} else if (Battery.chargeState == 1) {
|
||||
return Translation.tr("Charging:");
|
||||
} else {
|
||||
return Translation.tr("Discharging:");
|
||||
}
|
||||
}
|
||||
value: {
|
||||
if (Battery.chargeState == 4) {
|
||||
return "";
|
||||
} else {
|
||||
return `${Battery.energyRate.toFixed(2)}W`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledPopupValueRow {
|
||||
icon: "heart_check"
|
||||
label: Translation.tr("Health:")
|
||||
value: `${(Battery.health).toFixed(1)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
RippleButton {
|
||||
id: button
|
||||
|
||||
required default property Item content
|
||||
property bool extraActiveCondition: false
|
||||
|
||||
implicitHeight: Math.max(content.implicitHeight, 26, content.implicitHeight)
|
||||
implicitWidth: implicitHeight
|
||||
contentItem: content
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool borderless: Config.options.bar.borderless
|
||||
property bool showDate: Config.options.bar.verbose
|
||||
implicitWidth: rowLayout.implicitWidth
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer1
|
||||
text: DateTime.time
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.showDate
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer1
|
||||
text: "•"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.showDate
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer1
|
||||
text: DateTime.longDate
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
ClockWidgetPopup {
|
||||
hoverTarget: mouseArea
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledPopup {
|
||||
id: root
|
||||
property string formattedDate: Qt.locale().toString(DateTime.clock.date, "dddd, MMMM dd, yyyy")
|
||||
property string formattedTime: DateTime.time
|
||||
property string formattedUptime: DateTime.uptime
|
||||
property string todosSection: getUpcomingTodos()
|
||||
|
||||
function getUpcomingTodos() {
|
||||
const unfinishedTodos = Todo.list.filter(function (item) {
|
||||
return !item.done;
|
||||
});
|
||||
if (unfinishedTodos.length === 0) {
|
||||
return Translation.tr("No pending tasks");
|
||||
}
|
||||
|
||||
// Limit to first 5 todos to keep popup manageable
|
||||
const limitedTodos = unfinishedTodos.slice(0, 5);
|
||||
let todoText = limitedTodos.map(function (item, index) {
|
||||
return ` ${index + 1}. ${item.content}`;
|
||||
}).join('\n');
|
||||
|
||||
if (unfinishedTodos.length > 5) {
|
||||
todoText += `\n ${Translation.tr("... and %1 more").arg(unfinishedTodos.length - 5)}`;
|
||||
}
|
||||
|
||||
return todoText;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
StyledPopupHeaderRow {
|
||||
icon: "calendar_month"
|
||||
label: root.formattedDate
|
||||
}
|
||||
|
||||
StyledPopupValueRow {
|
||||
icon: "timelapse"
|
||||
label: Translation.tr("System uptime:")
|
||||
value: root.formattedUptime
|
||||
}
|
||||
|
||||
// Tasks
|
||||
Column {
|
||||
spacing: 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledPopupValueRow {
|
||||
icon: "checklist"
|
||||
label: Translation.tr("To Do:")
|
||||
value: ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
wrapMode: Text.Wrap
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
text: root.todosSection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
property bool vertical: false
|
||||
property color color: Appearance.colors.colOnSurfaceVariant
|
||||
active: HyprlandXkb.layoutCodes.length > 1
|
||||
visible: active
|
||||
|
||||
function abbreviateLayoutCode(fullCode) {
|
||||
return fullCode.split(':').map(layout => {
|
||||
const baseLayout = layout.split('-')[0];
|
||||
return baseLayout.slice(0, 4);
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitWidth: root.vertical ? null : layoutCodeText.implicitWidth
|
||||
implicitHeight: root.vertical ? layoutCodeText.implicitHeight : null
|
||||
|
||||
StyledText {
|
||||
id: layoutCodeText
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: abbreviateLayoutCode(HyprlandXkb.currentLayoutCode)
|
||||
font.pixelSize: text.includes("\n") ? Appearance.font.pixelSize.smallie : Appearance.font.pixelSize.small
|
||||
color: root.color
|
||||
animateChange: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import QtQuick
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
|
||||
property bool showPing: false
|
||||
|
||||
property real buttonPadding: 5
|
||||
implicitWidth: distroIcon.width + buttonPadding * 2
|
||||
implicitHeight: distroIcon.height + buttonPadding * 2
|
||||
buttonRadius: Appearance.rounding.full
|
||||
colBackgroundHover: Appearance.colors.colLayer1Hover
|
||||
colRipple: Appearance.colors.colLayer1Active
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
toggled: GlobalStates.sidebarLeftOpen
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Ai
|
||||
function onResponseFinished() {
|
||||
if (GlobalStates.sidebarLeftOpen) return;
|
||||
root.showPing = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Booru
|
||||
function onResponseFinished() {
|
||||
if (GlobalStates.sidebarLeftOpen) return;
|
||||
root.showPing = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onSidebarLeftOpenChanged() {
|
||||
root.showPing = false;
|
||||
}
|
||||
}
|
||||
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
anchors.centerIn: parent
|
||||
width: 19.5
|
||||
height: 19.5
|
||||
source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : `${Config.options.bar.topLeftIcon}-symbolic`
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
|
||||
Rectangle {
|
||||
opacity: root.showPing ? 1 : 0
|
||||
visible: opacity > 0
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
bottomMargin: -2
|
||||
rightMargin: -2
|
||||
}
|
||||
implicitWidth: 8
|
||||
implicitHeight: 8
|
||||
radius: Appearance.rounding.full
|
||||
color: Appearance.colors.colTertiary
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs
|
||||
import qs.modules.common.functions
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool borderless: Config.options.bar.borderless
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) || Translation.tr("No media")
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
|
||||
Timer {
|
||||
running: activePlayer?.playbackState == MprisPlaybackState.Playing
|
||||
interval: Config.options.resources.updateInterval
|
||||
repeat: true
|
||||
onTriggered: activePlayer.positionChanged()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton | Qt.RightButton | Qt.LeftButton
|
||||
onPressed: (event) => {
|
||||
if (event.button === Qt.MiddleButton) {
|
||||
activePlayer.togglePlaying();
|
||||
} else if (event.button === Qt.BackButton) {
|
||||
activePlayer.previous();
|
||||
} else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) {
|
||||
activePlayer.next();
|
||||
} else if (event.button === Qt.LeftButton) {
|
||||
GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout { // Real content
|
||||
id: rowLayout
|
||||
|
||||
spacing: 4
|
||||
anchors.fill: parent
|
||||
|
||||
ClippedFilledCircularProgress {
|
||||
id: mediaCircProg
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
lineWidth: Appearance.rounding.unsharpen
|
||||
value: activePlayer?.position / activePlayer?.length
|
||||
implicitSize: 20
|
||||
colPrimary: Appearance.colors.colOnSecondaryContainer
|
||||
enableAnimation: false
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: mediaCircProg.implicitSize
|
||||
height: mediaCircProg.implicitSize
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
fill: 1
|
||||
text: activePlayer?.isPlaying ? "pause" : "music_note"
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: Config.options.bar.verbose
|
||||
width: rowLayout.width - (CircularProgress.size + rowLayout.spacing * 2)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true // Ensures the text takes up available space
|
||||
Layout.rightMargin: rowLayout.spacing
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight // Truncates the text on the right
|
||||
color: Appearance.colors.colOnLayer1
|
||||
text: `${cleanedTitle}${activePlayer?.trackArtist ? ' • ' + activePlayer.trackArtist : ''}`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
MaterialSymbol {
|
||||
id: root
|
||||
readonly property bool showUnreadCount: Config.options.bar.indicators.notifications.showUnreadCount
|
||||
text: Notifications.silent ? "notifications_paused" : "notifications"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
|
||||
Rectangle {
|
||||
id: notifPing
|
||||
visible: !Notifications.silent && Notifications.unread > 0
|
||||
anchors {
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
rightMargin: root.showUnreadCount ? 0 : 1
|
||||
topMargin: root.showUnreadCount ? 0 : 3
|
||||
}
|
||||
radius: Appearance.rounding.full
|
||||
color: Appearance.colors.colOnLayer0
|
||||
z: 1
|
||||
|
||||
implicitHeight: root.showUnreadCount ? Math.max(notificationCounterText.implicitWidth, notificationCounterText.implicitHeight) : 8
|
||||
implicitWidth: implicitHeight
|
||||
|
||||
StyledText {
|
||||
id: notificationCounterText
|
||||
visible: root.showUnreadCount
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Appearance.font.pixelSize.smallest
|
||||
color: Appearance.colors.colLayer0
|
||||
text: Notifications.unread
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property string iconName
|
||||
required property double percentage
|
||||
property int warningThreshold: 100
|
||||
property bool shown: true
|
||||
clip: true
|
||||
visible: width > 0 && height > 0
|
||||
implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
property bool warning: percentage * 100 >= warningThreshold
|
||||
|
||||
RowLayout {
|
||||
id: resourceRowLayout
|
||||
spacing: 2
|
||||
x: shown ? 0 : -resourceRowLayout.width
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
ClippedFilledCircularProgress {
|
||||
id: resourceCircProg
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
lineWidth: Appearance.rounding.unsharpen
|
||||
value: percentage
|
||||
implicitSize: 20
|
||||
colPrimary: root.warning ? Appearance.colors.colError : Appearance.colors.colOnSecondaryContainer
|
||||
accountForLightBleeding: !root.warning
|
||||
enableAnimation: false
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: resourceCircProg.implicitSize
|
||||
height: resourceCircProg.implicitSize
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
font.weight: Font.DemiBold
|
||||
fill: 1
|
||||
text: iconName
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: fullPercentageTextMetrics.width
|
||||
implicitHeight: percentageText.implicitHeight
|
||||
|
||||
TextMetrics {
|
||||
id: fullPercentageTextMetrics
|
||||
text: "100"
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: percentageText
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colOnLayer1
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
text: `${Math.round(percentage * 100).toString()}`
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: resourceRowLayout.x >= 0 && root.width > 0 && root.visible
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import qs.modules.common
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
property bool borderless: Config.options.bar.borderless
|
||||
property bool alwaysShowAllResources: false
|
||||
implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
hoverEnabled: true
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4
|
||||
anchors.rightMargin: 4
|
||||
|
||||
Resource {
|
||||
iconName: "memory"
|
||||
percentage: ResourceUsage.memoryUsedPercentage
|
||||
warningThreshold: Config.options.bar.resources.memoryWarningThreshold
|
||||
}
|
||||
|
||||
Resource {
|
||||
iconName: "swap_horiz"
|
||||
percentage: ResourceUsage.swapUsedPercentage
|
||||
shown: (Config.options.bar.resources.alwaysShowSwap && percentage > 0) ||
|
||||
(MprisController.activePlayer?.trackTitle == null) ||
|
||||
root.alwaysShowAllResources
|
||||
Layout.leftMargin: shown ? 6 : 0
|
||||
warningThreshold: Config.options.bar.resources.swapWarningThreshold
|
||||
}
|
||||
|
||||
Resource {
|
||||
iconName: "planner_review"
|
||||
percentage: ResourceUsage.cpuUsage
|
||||
shown: Config.options.bar.resources.alwaysShowCpu ||
|
||||
!(MprisController.activePlayer?.trackTitle?.length > 0) ||
|
||||
root.alwaysShowAllResources
|
||||
Layout.leftMargin: shown ? 6 : 0
|
||||
warningThreshold: Config.options.bar.resources.cpuWarningThreshold
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ResourcesPopup {
|
||||
hoverTarget: root
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledPopup {
|
||||
id: root
|
||||
|
||||
// Helper function to format KB to GB
|
||||
function formatKB(kb) {
|
||||
return (kb / (1024 * 1024)).toFixed(1) + " GB";
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
|
||||
Column {
|
||||
anchors.top: parent.top
|
||||
spacing: 8
|
||||
|
||||
StyledPopupHeaderRow {
|
||||
icon: "memory"
|
||||
label: "RAM"
|
||||
}
|
||||
Column {
|
||||
spacing: 4
|
||||
StyledPopupValueRow {
|
||||
icon: "clock_loader_60"
|
||||
label: Translation.tr("Used:")
|
||||
value: root.formatKB(ResourceUsage.memoryUsed)
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
icon: "check_circle"
|
||||
label: Translation.tr("Free:")
|
||||
value: root.formatKB(ResourceUsage.memoryFree)
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
icon: "empty_dashboard"
|
||||
label: Translation.tr("Total:")
|
||||
value: root.formatKB(ResourceUsage.memoryTotal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: ResourceUsage.swapTotal > 0
|
||||
anchors.top: parent.top
|
||||
spacing: 8
|
||||
|
||||
StyledPopupHeaderRow {
|
||||
icon: "swap_horiz"
|
||||
label: "Swap"
|
||||
}
|
||||
Column {
|
||||
spacing: 4
|
||||
StyledPopupValueRow {
|
||||
icon: "clock_loader_60"
|
||||
label: Translation.tr("Used:")
|
||||
value: root.formatKB(ResourceUsage.swapUsed)
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
icon: "check_circle"
|
||||
label: Translation.tr("Free:")
|
||||
value: root.formatKB(ResourceUsage.swapFree)
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
icon: "empty_dashboard"
|
||||
label: Translation.tr("Total:")
|
||||
value: root.formatKB(ResourceUsage.swapTotal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.top: parent.top
|
||||
spacing: 8
|
||||
|
||||
StyledPopupHeaderRow {
|
||||
icon: "planner_review"
|
||||
label: "CPU"
|
||||
}
|
||||
Column {
|
||||
spacing: 4
|
||||
StyledPopupValueRow {
|
||||
icon: "bolt"
|
||||
label: Translation.tr("Load:")
|
||||
value: `${Math.round(ResourceUsage.cpuUsage * 100)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Revealer { // Scroll hint
|
||||
id: root
|
||||
property string icon
|
||||
property string side: "left"
|
||||
property string tooltipText: ""
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.right: root.side === "left" ? parent.right : undefined
|
||||
anchors.left: root.side === "right" ? parent.left : undefined
|
||||
implicitWidth: contentColumn.implicitWidth
|
||||
implicitHeight: contentColumn.implicitHeight
|
||||
property bool hovered: false
|
||||
|
||||
hoverEnabled: true
|
||||
onEntered: hovered = true
|
||||
onExited: hovered = false
|
||||
acceptedButtons: Qt.NoButton
|
||||
|
||||
property bool showHintTimedOut: false
|
||||
onHoveredChanged: showHintTimedOut = false
|
||||
Timer {
|
||||
running: mouseArea.hovered
|
||||
interval: 500
|
||||
onTriggered: mouseArea.showHintTimedOut = true
|
||||
}
|
||||
|
||||
PopupToolTip {
|
||||
extraVisibleCondition: (tooltipText.length > 0 && mouseArea.showHintTimedOut)
|
||||
text: tooltipText
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
spacing: -5
|
||||
MaterialSymbol {
|
||||
text: "keyboard_arrow_up"
|
||||
iconSize: 14
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
MaterialSymbol {
|
||||
text: root.icon
|
||||
iconSize: 14
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
MaterialSymbol {
|
||||
text: "keyboard_arrow_down"
|
||||
iconSize: 14
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
LazyLoader {
|
||||
id: root
|
||||
|
||||
property Item hoverTarget
|
||||
default property Item contentItem
|
||||
property real popupBackgroundMargin: 0
|
||||
|
||||
active: hoverTarget && hoverTarget.containsMouse
|
||||
|
||||
component: PanelWindow {
|
||||
id: popupWindow
|
||||
color: "transparent"
|
||||
|
||||
anchors.left: !Config.options.bar.vertical || (Config.options.bar.vertical && !Config.options.bar.bottom)
|
||||
anchors.right: Config.options.bar.vertical && Config.options.bar.bottom
|
||||
anchors.top: Config.options.bar.vertical || (!Config.options.bar.vertical && !Config.options.bar.bottom)
|
||||
anchors.bottom: !Config.options.bar.vertical && Config.options.bar.bottom
|
||||
|
||||
implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + root.popupBackgroundMargin
|
||||
implicitHeight: popupBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + root.popupBackgroundMargin
|
||||
|
||||
mask: Region {
|
||||
item: popupBackground
|
||||
}
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
exclusiveZone: 0
|
||||
margins {
|
||||
left: {
|
||||
if (!Config.options.bar.vertical) return root.QsWindow?.mapFromItem(
|
||||
root.hoverTarget,
|
||||
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
|
||||
).x;
|
||||
return Appearance.sizes.verticalBarWidth
|
||||
}
|
||||
top: {
|
||||
if (!Config.options.bar.vertical) return Appearance.sizes.barHeight;
|
||||
return root.QsWindow?.mapFromItem(
|
||||
root.hoverTarget,
|
||||
(root.hoverTarget.height - popupBackground.implicitHeight) / 2, 0
|
||||
).y;
|
||||
}
|
||||
right: Appearance.sizes.verticalBarWidth
|
||||
bottom: Appearance.sizes.barHeight
|
||||
}
|
||||
WlrLayershell.namespace: "quickshell:popup"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: popupBackground
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: popupBackground
|
||||
readonly property real margin: 10
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.left)
|
||||
rightMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.right)
|
||||
topMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.top)
|
||||
bottomMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.bottom)
|
||||
}
|
||||
implicitWidth: root.contentItem.implicitWidth + margin * 2
|
||||
implicitHeight: root.contentItem.implicitHeight + margin * 2
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
children: [root.contentItem]
|
||||
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Row {
|
||||
id: root
|
||||
required property var icon
|
||||
required property var label
|
||||
spacing: 5
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fill: 0
|
||||
font.weight: Font.DemiBold
|
||||
text: root.icon
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.label
|
||||
font {
|
||||
weight: Font.DemiBold
|
||||
pixelSize: Appearance.font.pixelSize.normal
|
||||
}
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
required property string icon
|
||||
required property string label
|
||||
required property string value
|
||||
spacing: 4
|
||||
|
||||
MaterialSymbol {
|
||||
text: root.icon
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
}
|
||||
StyledText {
|
||||
text: root.label
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
visible: root.value !== ""
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
text: root.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.SystemTray
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: gridLayout.implicitWidth
|
||||
implicitHeight: gridLayout.implicitHeight
|
||||
property bool vertical: false
|
||||
property bool invertSide: false
|
||||
property bool trayOverflowOpen: false
|
||||
property bool showSeparator: true
|
||||
property bool showOverflowMenu: true
|
||||
property var activeMenu: null
|
||||
|
||||
property bool smartTray: Config.options.bar.tray.filterPassive
|
||||
property list<var> itemsInUserList: SystemTray.items.values.filter(i => (Config.options.bar.tray.pinnedItems.includes(i.id) && (!smartTray || i.status !== Status.Passive)))
|
||||
property list<var> itemsNotInUserList: SystemTray.items.values.filter(i => (!Config.options.bar.tray.pinnedItems.includes(i.id) && (!smartTray || i.status !== Status.Passive)))
|
||||
|
||||
property bool invertPins: Config.options.bar.tray.invertPinnedItems
|
||||
property list<var> pinnedItems: invertPins ? itemsNotInUserList : itemsInUserList
|
||||
property list<var> unpinnedItems: invertPins ? itemsInUserList : itemsNotInUserList
|
||||
onUnpinnedItemsChanged: {
|
||||
if (unpinnedItems.length == 0) root.closeOverflowMenu();
|
||||
}
|
||||
|
||||
function grabFocus() {
|
||||
focusGrab.active = true;
|
||||
}
|
||||
|
||||
function setExtraWindowAndGrabFocus(window) {
|
||||
root.activeMenu = window;
|
||||
root.grabFocus();
|
||||
}
|
||||
|
||||
function releaseFocus() {
|
||||
focusGrab.active = false;
|
||||
}
|
||||
|
||||
function closeOverflowMenu() {
|
||||
focusGrab.active = false;
|
||||
}
|
||||
|
||||
onTrayOverflowOpenChanged: {
|
||||
if (root.trayOverflowOpen) {
|
||||
root.grabFocus();
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
active: false
|
||||
windows: [trayOverflowLayout.QsWindow?.window, root.activeMenu]
|
||||
onCleared: {
|
||||
root.trayOverflowOpen = false;
|
||||
if (root.activeMenu) {
|
||||
root.activeMenu.close();
|
||||
root.activeMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: gridLayout
|
||||
columns: root.vertical ? 1 : -1
|
||||
anchors.fill: parent
|
||||
rowSpacing: 8
|
||||
columnSpacing: 15
|
||||
|
||||
RippleButton {
|
||||
id: trayOverflowButton
|
||||
visible: root.showOverflowMenu && root.unpinnedItems.length > 0
|
||||
toggled: root.trayOverflowOpen
|
||||
property bool containsMouse: hovered
|
||||
|
||||
downAction: () => root.trayOverflowOpen = !root.trayOverflowOpen
|
||||
|
||||
Layout.fillHeight: !root.vertical
|
||||
Layout.fillWidth: root.vertical
|
||||
background.implicitWidth: 24
|
||||
background.implicitHeight: 24
|
||||
background.anchors.centerIn: this
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
text: "expand_more"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: root.trayOverflowOpen ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer2
|
||||
rotation: (root.trayOverflowOpen ? 180 : 0) - (90 * root.vertical) + (180 * root.invertSide)
|
||||
Behavior on rotation {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
StyledPopup {
|
||||
id: overflowPopup
|
||||
hoverTarget: trayOverflowButton
|
||||
active: root.trayOverflowOpen && root.unpinnedItems.length > 0
|
||||
|
||||
GridLayout {
|
||||
id: trayOverflowLayout
|
||||
anchors.centerIn: parent
|
||||
columns: Math.ceil(Math.sqrt(root.unpinnedItems.length))
|
||||
columnSpacing: 10
|
||||
rowSpacing: 10
|
||||
|
||||
Repeater {
|
||||
model: root.unpinnedItems
|
||||
|
||||
delegate: SysTrayItem {
|
||||
required property SystemTrayItem modelData
|
||||
item: modelData
|
||||
Layout.fillHeight: !root.vertical
|
||||
Layout.fillWidth: root.vertical
|
||||
onMenuClosed: root.releaseFocus();
|
||||
onMenuOpened: (qsWindow) => root.setExtraWindowAndGrabFocus(qsWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.pinnedItems
|
||||
}
|
||||
|
||||
delegate: SysTrayItem {
|
||||
required property SystemTrayItem modelData
|
||||
item: modelData
|
||||
Layout.fillHeight: !root.vertical
|
||||
Layout.fillWidth: root.vertical
|
||||
onMenuClosed: root.releaseFocus();
|
||||
onMenuOpened: (qsWindow) => {
|
||||
root.setExtraWindowAndGrabFocus(qsWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.colors.colSubtext
|
||||
text: "•"
|
||||
visible: root.showSeparator && SystemTray.items.values.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
required property SystemTrayItem item
|
||||
property bool targetMenuOpen: false
|
||||
|
||||
signal menuOpened(qsWindow: var)
|
||||
signal menuClosed()
|
||||
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
onPressed: (event) => {
|
||||
switch (event.button) {
|
||||
case Qt.LeftButton:
|
||||
item.activate();
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
if (item.hasMenu) menu.open();
|
||||
break;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
onEntered: {
|
||||
tooltip.text = item.tooltipTitle.length > 0 ? item.tooltipTitle
|
||||
: (item.title.length > 0 ? item.title : item.id);
|
||||
if (item.tooltipDescription.length > 0) tooltip.text += " • " + item.tooltipDescription;
|
||||
if (Config.options.bar.tray.showItemId) tooltip.text += "\n[" + item.id + "]";
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: menu
|
||||
function open() {
|
||||
menu.active = true;
|
||||
}
|
||||
active: false
|
||||
sourceComponent: SysTrayMenu {
|
||||
Component.onCompleted: this.open();
|
||||
trayItemMenuHandle: root.item.menu
|
||||
anchor {
|
||||
window: root.QsWindow.window
|
||||
rect.x: root.x + (Config.options.bar.vertical ? 0 : QsWindow.window?.width)
|
||||
rect.y: root.y + (Config.options.bar.vertical ? QsWindow.window?.height : 0)
|
||||
rect.height: root.height
|
||||
rect.width: root.width
|
||||
edges: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
|
||||
gravity: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
|
||||
}
|
||||
onMenuOpened: (window) => root.menuOpened(window);
|
||||
onMenuClosed: {
|
||||
root.menuClosed();
|
||||
menu.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
visible: !Config.options.bar.tray.monochromeIcons
|
||||
source: root.item.icon
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.tray.monochromeIcons
|
||||
anchors.fill: trayIcon
|
||||
sourceComponent: Item {
|
||||
Desaturate {
|
||||
id: desaturatedIcon
|
||||
visible: false // There's already color overlay
|
||||
anchors.fill: parent
|
||||
source: trayIcon
|
||||
desaturation: 0.8 // 1.0 means fully grayscale
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: desaturatedIcon
|
||||
source: desaturatedIcon
|
||||
color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.9)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupToolTip {
|
||||
id: tooltip
|
||||
extraVisibleCondition: root.containsMouse
|
||||
alternativeVisibleCondition: extraVisibleCondition
|
||||
anchorEdges: (!Config.options.bar.bottom && !Config.options.bar.vertical) ? Edges.Bottom : Edges.Top
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
required property QsMenuHandle trayItemMenuHandle
|
||||
property real popupBackgroundMargin: 0
|
||||
|
||||
signal menuClosed
|
||||
signal menuOpened(qsWindow: var) // Correct type is QsWindow, but QML does not like that
|
||||
|
||||
color: "transparent"
|
||||
property real padding: Appearance.sizes.elevationMargin
|
||||
|
||||
implicitHeight: {
|
||||
let result = 0;
|
||||
for (let child of stackView.children) {
|
||||
result = Math.max(child.implicitHeight, result);
|
||||
}
|
||||
return result + popupBackground.padding * 2 + root.padding * 2;
|
||||
}
|
||||
implicitWidth: {
|
||||
let result = 0;
|
||||
for (let child of stackView.children) {
|
||||
result = Math.max(child.implicitWidth, result);
|
||||
}
|
||||
return result + popupBackground.padding * 2 + root.padding * 2;
|
||||
}
|
||||
|
||||
function open() {
|
||||
root.visible = true;
|
||||
root.menuOpened(root);
|
||||
}
|
||||
|
||||
function close() {
|
||||
root.visible = false;
|
||||
while (stackView.depth > 1)
|
||||
stackView.pop();
|
||||
root.menuClosed();
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.BackButton | Qt.RightButton
|
||||
onPressed: event => {
|
||||
if ((event.button === Qt.BackButton || event.button === Qt.RightButton) && stackView.depth > 1)
|
||||
stackView.pop();
|
||||
}
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: popupBackground
|
||||
opacity: popupBackground.opacity
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: popupBackground
|
||||
readonly property real padding: 4
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: Config.options.bar.vertical ? parent.verticalCenter : undefined
|
||||
top: Config.options.bar.vertical ? undefined : Config.options.bar.bottom ? undefined : parent.top
|
||||
bottom: Config.options.bar.vertical ? undefined : Config.options.bar.bottom ? parent.bottom : undefined
|
||||
margins: root.padding
|
||||
}
|
||||
|
||||
color: Appearance.colors.colLayer0
|
||||
radius: Appearance.rounding.windowRounding
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
clip: true
|
||||
|
||||
opacity: 0
|
||||
Component.onCompleted: opacity = 1
|
||||
implicitWidth: stackView.implicitWidth + popupBackground.padding * 2
|
||||
implicitHeight: stackView.implicitHeight + popupBackground.padding * 2
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: popupBackground.padding
|
||||
}
|
||||
pushEnter: NoAnim {}
|
||||
pushExit: NoAnim {}
|
||||
popEnter: NoAnim {}
|
||||
popExit: NoAnim {}
|
||||
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
|
||||
initialItem: SubMenu {
|
||||
handle: root.trayItemMenuHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NoAnim: Transition {
|
||||
NumberAnimation {
|
||||
duration: 0
|
||||
}
|
||||
}
|
||||
|
||||
component SubMenu: ColumnLayout {
|
||||
id: submenu
|
||||
required property QsMenuHandle handle
|
||||
property bool isSubMenu: false
|
||||
property bool shown: false
|
||||
opacity: shown ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Component.onCompleted: shown = true
|
||||
StackView.onActivating: shown = true
|
||||
StackView.onDeactivating: shown = false
|
||||
StackView.onRemoved: destroy()
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
menu: submenu.handle
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
visible: submenu.isSubMenu
|
||||
active: visible
|
||||
sourceComponent: RippleButton {
|
||||
id: backButton
|
||||
buttonRadius: popupBackground.radius - popupBackground.padding
|
||||
horizontalPadding: 12
|
||||
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: 36
|
||||
|
||||
downAction: () => stackView.pop()
|
||||
|
||||
contentItem: RowLayout {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: backButton.horizontalPadding
|
||||
rightMargin: backButton.horizontalPadding
|
||||
}
|
||||
spacing: 8
|
||||
MaterialSymbol {
|
||||
iconSize: 20
|
||||
text: "chevron_left"
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: Translation.tr("Back")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: menuEntriesRepeater
|
||||
property bool iconColumnNeeded: {
|
||||
for (let i = 0; i < menuOpener.children.values.length; i++) {
|
||||
if (menuOpener.children.values[i].icon.length > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
property bool specialInteractionColumnNeeded: {
|
||||
for (let i = 0; i < menuOpener.children.values.length; i++) {
|
||||
if (menuOpener.children.values[i].buttonType !== QsMenuButtonType.None)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
model: menuOpener.children
|
||||
delegate: SysTrayMenuEntry {
|
||||
required property QsMenuEntry modelData
|
||||
forceIconColumn: menuEntriesRepeater.iconColumnNeeded
|
||||
forceSpecialInteractionColumn: menuEntriesRepeater.specialInteractionColumnNeeded
|
||||
menuEntry: modelData
|
||||
|
||||
buttonRadius: popupBackground.radius - popupBackground.padding
|
||||
|
||||
onDismiss: root.close()
|
||||
onOpenSubmenu: handle => {
|
||||
stackView.push(subMenuComponent.createObject(null, {
|
||||
handle: handle,
|
||||
isSubMenu: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: subMenuComponent
|
||||
SubMenu {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
required property QsMenuEntry menuEntry
|
||||
property bool forceIconColumn: false
|
||||
property bool forceSpecialInteractionColumn: false
|
||||
readonly property bool hasIcon: menuEntry.icon.length > 0
|
||||
readonly property bool hasSpecialInteraction: menuEntry.buttonType !== QsMenuButtonType.None
|
||||
|
||||
signal dismiss()
|
||||
signal openSubmenu(handle: QsMenuHandle)
|
||||
|
||||
colBackground: menuEntry.isSeparator ? Appearance.m3colors.m3outlineVariant : ColorUtils.transparentize(Appearance.colors.colLayer0)
|
||||
enabled: !menuEntry.isSeparator
|
||||
opacity: 1
|
||||
|
||||
horizontalPadding: 12
|
||||
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: menuEntry.isSeparator ? 1 : 36
|
||||
Layout.topMargin: menuEntry.isSeparator ? 4 : 0
|
||||
Layout.bottomMargin: menuEntry.isSeparator ? 4 : 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
Component.onCompleted: {
|
||||
if (menuEntry.isSeparator) {
|
||||
root.buttonColor = root.colBackground;
|
||||
}
|
||||
}
|
||||
|
||||
releaseAction: () => {
|
||||
if (menuEntry.hasChildren) {
|
||||
root.openSubmenu(root.menuEntry);
|
||||
return;
|
||||
}
|
||||
menuEntry.triggered();
|
||||
root.dismiss();
|
||||
}
|
||||
altAction: (event) => { // Not hog right-click
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: contentItem
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: root.horizontalPadding
|
||||
rightMargin: root.horizontalPadding
|
||||
}
|
||||
spacing: 8
|
||||
visible: !root.menuEntry.isSeparator
|
||||
|
||||
// Interaction: checkbox or radio button
|
||||
Item {
|
||||
visible: root.hasSpecialInteraction || root.forceSpecialInteractionColumn
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: root.menuEntry.buttonType === QsMenuButtonType.RadioButton
|
||||
|
||||
sourceComponent: StyledRadioButton {
|
||||
enabled: false
|
||||
padding: 0
|
||||
checked: root.menuEntry.checkState === Qt.Checked
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: root.menuEntry.buttonType === QsMenuButtonType.CheckBox && root.menuEntry.checkState !== Qt.Unchecked
|
||||
|
||||
sourceComponent: MaterialSymbol {
|
||||
text: root.menuEntry.checkState === Qt.PartiallyChecked ? "check_indeterminate_small" : "check"
|
||||
iconSize: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Button icon
|
||||
Item {
|
||||
visible: root.hasIcon || root.forceIconColumn
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
active: root.menuEntry.icon.length > 0
|
||||
sourceComponent: IconImage {
|
||||
asynchronous: true
|
||||
source: root.menuEntry.icon
|
||||
implicitSize: 20
|
||||
mipmap: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
text: root.menuEntry.text
|
||||
font.pixelSize: Appearance.font.pixelSize.smallie
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.menuEntry.hasChildren
|
||||
|
||||
sourceComponent: MaterialSymbol {
|
||||
text: "chevron_right"
|
||||
iconSize: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool borderless: Config.options.bar.borderless
|
||||
implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
|
||||
implicitHeight: rowLayout.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
spacing: 4
|
||||
anchors.centerIn: parent
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.utilButtons.showScreenSnip
|
||||
visible: Config.options.bar.utilButtons.showScreenSnip
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 1
|
||||
text: "screenshot_region"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.utilButtons.showScreenRecord
|
||||
visible: Config.options.bar.utilButtons.showScreenRecord
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: Quickshell.execDetached([Directories.recordScriptPath])
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 1
|
||||
text: "videocam"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.utilButtons.showColorPicker
|
||||
visible: Config.options.bar.utilButtons.showColorPicker
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: Quickshell.execDetached(["hyprpicker", "-a"])
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 1
|
||||
text: "colorize"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.utilButtons.showKeyboardToggle
|
||||
visible: Config.options.bar.utilButtons.showKeyboardToggle
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: GlobalStates.oskOpen = !GlobalStates.oskOpen
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 0
|
||||
text: "keyboard"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.utilButtons.showMicToggle
|
||||
visible: Config.options.bar.utilButtons.showMicToggle
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_SOURCE@", "toggle"])
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 0
|
||||
text: Pipewire.defaultAudioSource?.audio?.muted ? "mic_off" : "mic"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.utilButtons.showDarkModeToggle
|
||||
visible: Config.options.bar.utilButtons.showDarkModeToggle
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: event => {
|
||||
if (Appearance.m3colors.darkmode) {
|
||||
Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`);
|
||||
} else {
|
||||
Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`);
|
||||
}
|
||||
}
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 0
|
||||
text: Appearance.m3colors.darkmode ? "light_mode" : "dark_mode"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.utilButtons.showPerformanceProfileToggle
|
||||
visible: Config.options.bar.utilButtons.showPerformanceProfileToggle
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: event => {
|
||||
if (PowerProfiles.hasPerformanceProfile) {
|
||||
switch(PowerProfiles.profile) {
|
||||
case PowerProfile.PowerSaver: PowerProfiles.profile = PowerProfile.Balanced
|
||||
break;
|
||||
case PowerProfile.Balanced: PowerProfiles.profile = PowerProfile.Performance
|
||||
break;
|
||||
case PowerProfile.Performance: PowerProfiles.profile = PowerProfile.PowerSaver
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
PowerProfiles.profile = PowerProfiles.profile == PowerProfile.Balanced ? PowerProfile.PowerSaver : PowerProfile.Balanced
|
||||
}
|
||||
}
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 0
|
||||
text: switch(PowerProfiles.profile) {
|
||||
case PowerProfile.PowerSaver: return "energy_savings_leaf"
|
||||
case PowerProfile.Balanced: return "settings_slow_motion"
|
||||
case PowerProfile.Performance: return "local_fire_department"
|
||||
}
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.models
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property bool vertical: false
|
||||
property bool borderless: Config.options.bar.borderless
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
|
||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
||||
|
||||
readonly property int workspacesShown: Config.options.bar.workspaces.shown
|
||||
readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 1) / root.workspacesShown)
|
||||
property list<bool> workspaceOccupied: []
|
||||
property int widgetPadding: 4
|
||||
property int workspaceButtonWidth: 26
|
||||
property real activeWorkspaceMargin: 2
|
||||
property real workspaceIconSize: workspaceButtonWidth * 0.69
|
||||
property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
|
||||
property real workspaceIconOpacityShrinked: 1
|
||||
property real workspaceIconMarginShrinked: -4
|
||||
property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % root.workspacesShown
|
||||
|
||||
property bool showNumbers: false
|
||||
Timer {
|
||||
id: showNumbersTimer
|
||||
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.showNumbers = true
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onSuperDownChanged() {
|
||||
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
|
||||
if (GlobalStates.superDown) showNumbersTimer.restart();
|
||||
else {
|
||||
showNumbersTimer.stop();
|
||||
root.showNumbers = false;
|
||||
}
|
||||
}
|
||||
function onSuperReleaseMightTriggerChanged() {
|
||||
showNumbersTimer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update workspaceOccupied
|
||||
function updateWorkspaceOccupied() {
|
||||
workspaceOccupied = Array.from({ length: root.workspacesShown }, (_, i) => {
|
||||
return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * root.workspacesShown + i + 1);
|
||||
})
|
||||
}
|
||||
|
||||
// Occupied workspace updates
|
||||
Component.onCompleted: updateWorkspaceOccupied()
|
||||
Connections {
|
||||
target: Hyprland.workspaces
|
||||
function onValuesChanged() {
|
||||
updateWorkspaceOccupied();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Hyprland
|
||||
function onFocusedWorkspaceChanged() {
|
||||
updateWorkspaceOccupied();
|
||||
}
|
||||
}
|
||||
onWorkspaceGroupChanged: {
|
||||
updateWorkspaceOccupied();
|
||||
}
|
||||
|
||||
implicitWidth: root.vertical ? Appearance.sizes.verticalBarWidth : (root.workspaceButtonWidth * root.workspacesShown)
|
||||
implicitHeight: root.vertical ? (root.workspaceButtonWidth * root.workspacesShown) : Appearance.sizes.barHeight
|
||||
|
||||
// Scroll to switch workspaces
|
||||
WheelHandler {
|
||||
onWheel: (event) => {
|
||||
if (event.angleDelta.y < 0)
|
||||
Hyprland.dispatch(`workspace r+1`);
|
||||
else if (event.angleDelta.y > 0)
|
||||
Hyprland.dispatch(`workspace r-1`);
|
||||
}
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.BackButton
|
||||
onPressed: (event) => {
|
||||
if (event.button === Qt.BackButton) {
|
||||
Hyprland.dispatch(`togglespecialworkspace`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspaces - background
|
||||
Grid {
|
||||
z: 1
|
||||
anchors.centerIn: parent
|
||||
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
columns: root.vertical ? 1 : root.workspacesShown
|
||||
rows: root.vertical ? root.workspacesShown : 1
|
||||
|
||||
Repeater {
|
||||
model: root.workspacesShown
|
||||
|
||||
Rectangle {
|
||||
z: 1
|
||||
implicitWidth: workspaceButtonWidth
|
||||
implicitHeight: workspaceButtonWidth
|
||||
radius: (width / 2)
|
||||
property var previousOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index))
|
||||
property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+2))
|
||||
property var radiusPrev: previousOccupied ? 0 : (width / 2)
|
||||
property var radiusNext: rightOccupied ? 0 : (width / 2)
|
||||
|
||||
topLeftRadius: radiusPrev
|
||||
bottomLeftRadius: root.vertical ? radiusNext : radiusPrev
|
||||
topRightRadius: root.vertical ? radiusPrev : radiusNext
|
||||
bottomRightRadius: radiusNext
|
||||
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
|
||||
opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+1)) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on radiusPrev {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on radiusNext {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Active workspace
|
||||
Rectangle {
|
||||
z: 2
|
||||
// Make active ws indicator, which has a brighter color, smaller to look like it is of the same size as ws occupied highlight
|
||||
radius: Appearance.rounding.full
|
||||
color: Appearance.colors.colPrimary
|
||||
|
||||
anchors {
|
||||
verticalCenter: vertical ? undefined : parent.verticalCenter
|
||||
horizontalCenter: vertical ? parent.horizontalCenter : undefined
|
||||
}
|
||||
|
||||
AnimatedTabIndexPair {
|
||||
id: idxPair
|
||||
index: root.workspaceIndexInGroup
|
||||
}
|
||||
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * workspaceButtonWidth + root.activeWorkspaceMargin
|
||||
property real indicatorLength: Math.abs(idxPair.idx1 - idxPair.idx2) * workspaceButtonWidth + workspaceButtonWidth - root.activeWorkspaceMargin * 2
|
||||
property real indicatorThickness: workspaceButtonWidth - root.activeWorkspaceMargin * 2
|
||||
|
||||
x: root.vertical ? null : indicatorPosition
|
||||
implicitWidth: root.vertical ? indicatorThickness : indicatorLength
|
||||
y: root.vertical ? indicatorPosition : null
|
||||
implicitHeight: root.vertical ? indicatorLength : indicatorThickness
|
||||
|
||||
}
|
||||
|
||||
// Workspaces - numbers
|
||||
Grid {
|
||||
z: 3
|
||||
|
||||
columns: root.vertical ? 1 : root.workspacesShown
|
||||
rows: root.vertical ? root.workspacesShown : 1
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Repeater {
|
||||
model: root.workspacesShown
|
||||
|
||||
Button {
|
||||
id: button
|
||||
property int workspaceValue: workspaceGroup * root.workspacesShown + index + 1
|
||||
implicitHeight: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.barHeight
|
||||
implicitWidth: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.verticalBarWidth
|
||||
onPressed: Hyprland.dispatch(`workspace ${workspaceValue}`)
|
||||
width: vertical ? undefined : workspaceButtonWidth
|
||||
height: vertical ? workspaceButtonWidth : undefined
|
||||
|
||||
background: Item {
|
||||
id: workspaceButtonBackground
|
||||
implicitWidth: workspaceButtonWidth
|
||||
implicitHeight: workspaceButtonWidth
|
||||
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(button.workspaceValue)
|
||||
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
|
||||
|
||||
StyledText { // Workspace number text
|
||||
opacity: root.showNumbers
|
||||
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || root.showNumbers))
|
||||
|| (root.showNumbers && !Config.options?.bar.workspaces.showAppIcons)
|
||||
) ? 1 : 0
|
||||
z: 3
|
||||
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
|
||||
family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : defaultFont
|
||||
}
|
||||
text: Config.options?.bar.workspaces.numberMap[button.workspaceValue - 1] || button.workspaceValue
|
||||
elide: Text.ElideRight
|
||||
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
|
||||
Appearance.m3colors.m3onPrimary :
|
||||
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
|
||||
Appearance.colors.colOnLayer1Inactive)
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
Rectangle { // Dot instead of ws number
|
||||
id: wsDot
|
||||
opacity: (Config.options?.bar.workspaces.alwaysShowNumbers
|
||||
|| root.showNumbers
|
||||
|| (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
|
||||
) ? 0 : 1
|
||||
visible: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
width: workspaceButtonWidth * 0.18
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
|
||||
Appearance.m3colors.m3onPrimary :
|
||||
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
|
||||
Appearance.colors.colOnLayer1Inactive)
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
Item { // Main app icon
|
||||
anchors.centerIn: parent
|
||||
width: workspaceButtonWidth
|
||||
height: workspaceButtonWidth
|
||||
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 :
|
||||
(workspaceButtonBackground.biggestWindow && !root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
|
||||
1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0
|
||||
visible: opacity > 0
|
||||
IconImage {
|
||||
id: mainAppIcon
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.bottomMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
|
||||
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
|
||||
anchors.rightMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
|
||||
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
|
||||
|
||||
source: workspaceButtonBackground.mainAppIconSource
|
||||
implicitSize: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on anchors.bottomMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on anchors.rightMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitSize {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.workspaces.monochromeIcons
|
||||
anchors.fill: mainAppIcon
|
||||
sourceComponent: Item {
|
||||
Desaturate {
|
||||
id: desaturatedIcon
|
||||
visible: false // There's already color overlay
|
||||
anchors.fill: parent
|
||||
source: mainAppIcon
|
||||
desaturation: 0.8
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: desaturatedIcon
|
||||
source: desaturatedIcon
|
||||
color: ColorUtils.transparentize(wsDot.color, 0.9)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
property bool hovered: false
|
||||
implicitWidth: rowLayout.implicitWidth + 10 * 2
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
onPressed: {
|
||||
Weather.getData();
|
||||
Quickshell.execDetached(["notify-send",
|
||||
Translation.tr("Weather"),
|
||||
Translation.tr("Refreshing (manually triggered)")
|
||||
, "-a", "Shell"
|
||||
])
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.centerIn: parent
|
||||
|
||||
MaterialSymbol {
|
||||
fill: 0
|
||||
text: Icons.getWeatherIcon(Weather.data.wCode) ?? "cloud"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer1
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: true
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer1
|
||||
text: Weather.data?.temp ?? "--°"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
WeatherPopup {
|
||||
id: weatherPopup
|
||||
hoverTarget: root
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
radius: Appearance.rounding.small
|
||||
color: Appearance.colors.colSurfaceContainerHigh
|
||||
implicitWidth: columnLayout.implicitWidth + 14 * 2
|
||||
implicitHeight: columnLayout.implicitHeight + 14 * 2
|
||||
Layout.fillWidth: parent
|
||||
|
||||
property alias title: title.text
|
||||
property alias value: value.text
|
||||
property alias symbol: symbol.text
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.fill: parent
|
||||
spacing: -10
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
MaterialSymbol {
|
||||
id: symbol
|
||||
fill: 0
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
StyledText {
|
||||
id: title
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: value
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.ii.bar
|
||||
|
||||
StyledPopup {
|
||||
id: root
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: Math.max(header.implicitWidth, gridLayout.implicitWidth)
|
||||
implicitHeight: gridLayout.implicitHeight
|
||||
spacing: 5
|
||||
|
||||
// Header
|
||||
ColumnLayout {
|
||||
id: header
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 6
|
||||
|
||||
MaterialSymbol {
|
||||
fill: 0
|
||||
font.weight: Font.Medium
|
||||
text: "location_on"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Weather.data.city
|
||||
font {
|
||||
weight: Font.Medium
|
||||
pixelSize: Appearance.font.pixelSize.normal
|
||||
}
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: temp
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
text: Weather.data.temp + " • " + Translation.tr("Feels like %1").arg(Weather.data.tempFeelsLike)
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics grid
|
||||
GridLayout {
|
||||
id: gridLayout
|
||||
columns: 2
|
||||
rowSpacing: 5
|
||||
columnSpacing: 5
|
||||
uniformCellWidths: true
|
||||
|
||||
WeatherCard {
|
||||
title: Translation.tr("UV Index")
|
||||
symbol: "wb_sunny"
|
||||
value: Weather.data.uv
|
||||
}
|
||||
WeatherCard {
|
||||
title: Translation.tr("Wind")
|
||||
symbol: "air"
|
||||
value: `(${Weather.data.windDir}) ${Weather.data.wind}`
|
||||
}
|
||||
WeatherCard {
|
||||
title: Translation.tr("Precipitation")
|
||||
symbol: "rainy_light"
|
||||
value: Weather.data.precip
|
||||
}
|
||||
WeatherCard {
|
||||
title: Translation.tr("Humidity")
|
||||
symbol: "humidity_low"
|
||||
value: Weather.data.humidity
|
||||
}
|
||||
WeatherCard {
|
||||
title: Translation.tr("Visibility")
|
||||
symbol: "visibility"
|
||||
value: Weather.data.visib
|
||||
}
|
||||
WeatherCard {
|
||||
title: Translation.tr("Pressure")
|
||||
symbol: "readiness_score"
|
||||
value: Weather.data.press
|
||||
}
|
||||
WeatherCard {
|
||||
title: Translation.tr("Sunrise")
|
||||
symbol: "wb_twilight"
|
||||
value: Weather.data.sunrise
|
||||
}
|
||||
WeatherCard {
|
||||
title: Translation.tr("Sunset")
|
||||
symbol: "bedtime"
|
||||
value: Weather.data.sunset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Qt.labs.synchronizer
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope { // Scope
|
||||
id: root
|
||||
property var tabButtonList: [
|
||||
{
|
||||
"icon": "keyboard",
|
||||
"name": Translation.tr("Keybinds")
|
||||
},
|
||||
{
|
||||
"icon": "experiment",
|
||||
"name": Translation.tr("Elements")
|
||||
},
|
||||
]
|
||||
|
||||
Loader {
|
||||
id: cheatsheetLoader
|
||||
active: false
|
||||
|
||||
sourceComponent: PanelWindow { // Window
|
||||
id: cheatsheetRoot
|
||||
visible: cheatsheetLoader.active
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
cheatsheetLoader.active = false;
|
||||
}
|
||||
exclusiveZone: 0
|
||||
implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2
|
||||
implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2
|
||||
WlrLayershell.namespace: "quickshell:cheatsheet"
|
||||
// Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
|
||||
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {
|
||||
item: cheatsheetBackground
|
||||
}
|
||||
|
||||
HyprlandFocusGrab { // Click outside to close
|
||||
id: grab
|
||||
windows: [cheatsheetRoot]
|
||||
active: cheatsheetRoot.visible
|
||||
onCleared: () => {
|
||||
if (!active)
|
||||
cheatsheetRoot.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Background
|
||||
StyledRectangularShadow {
|
||||
target: cheatsheetBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: cheatsheetBackground
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.windowRounding
|
||||
property real padding: 20
|
||||
implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2
|
||||
implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2
|
||||
|
||||
Keys.onPressed: event => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
cheatsheetRoot.hide();
|
||||
}
|
||||
if (event.modifiers === Qt.ControlModifier) {
|
||||
if (event.key === Qt.Key_PageDown) {
|
||||
tabBar.incrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
tabBar.decrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Tab) {
|
||||
tabBar.setCurrentIndex((tabBar.currentIndex + 1) % root.tabButtonList.length);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backtab) {
|
||||
tabBar.setCurrentIndex((tabBar.currentIndex - 1 + root.tabButtonList.length) % root.tabButtonList.length);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RippleButton { // Close button
|
||||
id: closeButton
|
||||
focus: cheatsheetRoot.visible
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
buttonRadius: Appearance.rounding.full
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
topMargin: 20
|
||||
rightMargin: 20
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
cheatsheetRoot.hide();
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.title
|
||||
text: "close"
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout { // Real content
|
||||
id: cheatsheetColumnLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
Toolbar {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enableShadow: false
|
||||
ToolbarTabBar {
|
||||
id: tabBar
|
||||
tabButtonList: root.tabButtonList
|
||||
currentIndex: swipeView.currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
SwipeView { // Content pages
|
||||
id: swipeView
|
||||
Layout.topMargin: 5
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: tabBar.currentIndex
|
||||
spacing: 10
|
||||
|
||||
implicitWidth: Math.max.apply(null, contentChildren.map(child => child.implicitWidth || 0))
|
||||
implicitHeight: Math.max.apply(null, contentChildren.map(child => child.implicitHeight || 0))
|
||||
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
|
||||
CheatsheetKeybinds {}
|
||||
CheatsheetPeriodicTable {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "cheatsheet"
|
||||
|
||||
function toggle(): void {
|
||||
cheatsheetLoader.active = !cheatsheetLoader.active;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
cheatsheetLoader.active = false;
|
||||
}
|
||||
|
||||
function open(): void {
|
||||
cheatsheetLoader.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "cheatsheetToggle"
|
||||
description: "Toggles cheatsheet on press"
|
||||
|
||||
onPressed: {
|
||||
cheatsheetLoader.active = !cheatsheetLoader.active;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "cheatsheetOpen"
|
||||
description: "Opens cheatsheet on press"
|
||||
|
||||
onPressed: {
|
||||
cheatsheetLoader.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "cheatsheetClose"
|
||||
description: "Closes cheatsheet on press"
|
||||
|
||||
onPressed: {
|
||||
cheatsheetLoader.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var keybinds: HyprlandKeybinds.keybinds
|
||||
property real spacing: 20
|
||||
property real titleSpacing: 7
|
||||
property real padding: 4
|
||||
implicitWidth: row.implicitWidth + padding * 2
|
||||
implicitHeight: row.implicitHeight + padding * 2
|
||||
// Excellent symbol explaination and source :
|
||||
// http://xahlee.info/comp/unicode_computing_symbols.html
|
||||
// https://www.nerdfonts.com/cheat-sheet
|
||||
property var macSymbolMap: ({
|
||||
"Ctrl": "",
|
||||
"Alt": "",
|
||||
"Shift": "",
|
||||
"Space": "",
|
||||
"Tab": "↹",
|
||||
"Equal": "",
|
||||
"Minus": "",
|
||||
"Print": "",
|
||||
"BackSpace": "",
|
||||
"Delete": "⌦",
|
||||
"Return": "",
|
||||
"Period": ".",
|
||||
"Escape": "⎋"
|
||||
})
|
||||
property var functionSymbolMap: ({
|
||||
"F1": "",
|
||||
"F2": "",
|
||||
"F3": "",
|
||||
"F4": "",
|
||||
"F5": "",
|
||||
"F6": "",
|
||||
"F7": "",
|
||||
"F8": "",
|
||||
"F9": "",
|
||||
"F10": "",
|
||||
"F11": "",
|
||||
"F12": "",
|
||||
})
|
||||
|
||||
property var mouseSymbolMap: ({
|
||||
"mouse_up": "",
|
||||
"mouse_down": "",
|
||||
"mouse:272": "L",
|
||||
"mouse:273": "R",
|
||||
"Scroll ↑/↓": "",
|
||||
"Page_↑/↓": "⇞/⇟",
|
||||
})
|
||||
|
||||
property var keyBlacklist: ["Super_L"]
|
||||
property var keySubstitutions: Object.assign({
|
||||
"Super": "",
|
||||
"mouse_up": "Scroll ↓", // ikr, weird
|
||||
"mouse_down": "Scroll ↑", // trust me bro
|
||||
"mouse:272": "LMB",
|
||||
"mouse:273": "RMB",
|
||||
"mouse:275": "MouseBack",
|
||||
"Slash": "/",
|
||||
"Hash": "#",
|
||||
"Return": "Enter",
|
||||
// "Shift": "",
|
||||
},
|
||||
!!Config.options.cheatsheet.superKey ? {
|
||||
"Super": Config.options.cheatsheet.superKey,
|
||||
}: {},
|
||||
Config.options.cheatsheet.useMacSymbol ? macSymbolMap : {},
|
||||
Config.options.cheatsheet.useFnSymbol ? functionSymbolMap : {},
|
||||
Config.options.cheatsheet.useMouseSymbol ? mouseSymbolMap : {},
|
||||
)
|
||||
|
||||
Row { // Keybind columns
|
||||
id: row
|
||||
spacing: root.spacing
|
||||
|
||||
Repeater {
|
||||
model: keybinds.children
|
||||
|
||||
delegate: Column { // Keybind sections
|
||||
spacing: root.spacing
|
||||
required property var modelData
|
||||
anchors.top: row.top
|
||||
|
||||
Repeater {
|
||||
model: modelData.children
|
||||
|
||||
delegate: Item { // Section with real keybinds
|
||||
id: keybindSection
|
||||
required property var modelData
|
||||
implicitWidth: sectionColumn.implicitWidth
|
||||
implicitHeight: sectionColumn.implicitHeight
|
||||
|
||||
Column {
|
||||
id: sectionColumn
|
||||
anchors.centerIn: parent
|
||||
spacing: root.titleSpacing
|
||||
|
||||
StyledText {
|
||||
id: sectionTitle
|
||||
font {
|
||||
family: Appearance.font.family.title
|
||||
pixelSize: Appearance.font.pixelSize.title
|
||||
variableAxes: Appearance.font.variableAxes.title
|
||||
}
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: keybindSection.modelData.name
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: keybindGrid
|
||||
columns: 2
|
||||
columnSpacing: 4
|
||||
rowSpacing: 4
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
var result = [];
|
||||
for (var i = 0; i < keybindSection.modelData.keybinds.length; i++) {
|
||||
const keybind = keybindSection.modelData.keybinds[i];
|
||||
|
||||
if (!Config.options.cheatsheet.splitButtons) {
|
||||
for (var j = 0; j < keybind.mods.length; j++) {
|
||||
keybind.mods[j] = keySubstitutions[keybind.mods[j]] || keybind.mods[j];
|
||||
}
|
||||
keybind.mods = [keybind.mods.join(' ') ]
|
||||
keybind.mods[0] += !keyBlacklist.includes(keybind.key) && keybind.mods[0].length ? ' ' : ''
|
||||
keybind.mods[0] += !keyBlacklist.includes(keybind.key) ? (keySubstitutions[keybind.key] || keybind.key) : ''
|
||||
}
|
||||
|
||||
result.push({
|
||||
"type": "keys",
|
||||
"mods": keybind.mods,
|
||||
"key": keybind.key,
|
||||
});
|
||||
result.push({
|
||||
"type": "comment",
|
||||
"comment": keybind.comment,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
implicitWidth: keybindLoader.implicitWidth
|
||||
implicitHeight: keybindLoader.implicitHeight
|
||||
|
||||
Loader {
|
||||
id: keybindLoader
|
||||
sourceComponent: (modelData.type === "keys") ? keysComponent : commentComponent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: keysComponent
|
||||
Row {
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: modelData.mods
|
||||
delegate: KeyboardKey {
|
||||
required property var modelData
|
||||
key: keySubstitutions[modelData] || modelData
|
||||
pixelSize: Config.options.cheatsheet.fontSize.key
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: keybindPlus
|
||||
visible: Config.options.cheatsheet.splitButtons && !keyBlacklist.includes(modelData.key) && modelData.mods.length > 0
|
||||
text: "+"
|
||||
}
|
||||
KeyboardKey {
|
||||
id: keybindKey
|
||||
visible: Config.options.cheatsheet.splitButtons && !keyBlacklist.includes(modelData.key)
|
||||
key: keySubstitutions[modelData.key] || modelData.key
|
||||
pixelSize: Config.options.cheatsheet.fontSize.key
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: commentComponent
|
||||
Item {
|
||||
id: commentItem
|
||||
implicitWidth: commentText.implicitWidth + 8 * 2
|
||||
implicitHeight: commentText.implicitHeight
|
||||
|
||||
StyledText {
|
||||
id: commentText
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Config.options.cheatsheet.fontSize.comment || Appearance.font.pixelSize.smaller
|
||||
text: modelData.comment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import "periodic_table.js" as PTable
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var elements: PTable.elements
|
||||
readonly property var series: PTable.series
|
||||
property real spacing: 6
|
||||
implicitWidth: mainLayout.implicitWidth
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
Column {
|
||||
id: mainLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: root.spacing
|
||||
|
||||
Repeater { // Main table rows
|
||||
model: root.elements
|
||||
|
||||
delegate: Row { // Table cells
|
||||
id: tableRow
|
||||
spacing: root.spacing
|
||||
required property var modelData
|
||||
|
||||
Repeater {
|
||||
model: tableRow.modelData
|
||||
delegate: ElementTile {
|
||||
required property var modelData
|
||||
element: modelData
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gap
|
||||
implicitHeight: 20
|
||||
}
|
||||
|
||||
Repeater { // Main table rows
|
||||
model: root.series
|
||||
|
||||
delegate: Row { // Table cells
|
||||
id: seriesTableRow
|
||||
spacing: root.spacing
|
||||
required property var modelData
|
||||
|
||||
Repeater {
|
||||
model: seriesTableRow.modelData
|
||||
delegate: ElementTile {
|
||||
required property var modelData
|
||||
element: modelData
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
required property var element
|
||||
opacity: element.type != "empty" ? 1 : 0
|
||||
implicitHeight: 70
|
||||
implicitWidth: 70
|
||||
colBackground: Appearance.colors.colLayer2
|
||||
buttonRadius: Appearance.rounding.small
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
topMargin: 4
|
||||
leftMargin: 4
|
||||
}
|
||||
color: ColorUtils.transparentize(Appearance.colors.colLayer2)
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: Math.max(20, elementNumber.implicitWidth)
|
||||
implicitHeight: Math.max(20, elementNumber.implicitHeight)
|
||||
width: height
|
||||
|
||||
StyledText {
|
||||
id: elementNumber
|
||||
anchors.left: parent.left
|
||||
color: Appearance.colors.colOnLayer2
|
||||
text: root.element.number
|
||||
font.pixelSize: Appearance.font.pixelSize.smallest
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
topMargin: 4
|
||||
rightMargin: 4
|
||||
}
|
||||
color: ColorUtils.transparentize(Appearance.colors.colLayer2)
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: Math.max(20, elementWeight.implicitWidth)
|
||||
implicitHeight: Math.max(20, elementWeight.implicitHeight)
|
||||
width: height
|
||||
|
||||
StyledText {
|
||||
id: elementWeight
|
||||
anchors.right: parent.right
|
||||
color: Appearance.colors.colOnLayer2
|
||||
text: root.element.weight
|
||||
font.pixelSize: Appearance.font.pixelSize.smallest
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: elementSymbol
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colSecondary
|
||||
font.pixelSize: Appearance.font.pixelSize.huge
|
||||
text: root.element.symbol
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: elementName
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 4
|
||||
}
|
||||
font.pixelSize: Appearance.font.pixelSize.smallest
|
||||
color: Appearance.colors.colOnLayer2
|
||||
text: root.element.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// List of rows
|
||||
const elements = [
|
||||
[
|
||||
{ name: 'Hydrogen', symbol: 'H', number: 1, weight: 1.01, type: 'nonmetal' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: 'Helium', symbol: 'He', number: 2, weight: 4.00, type: 'noblegas' },
|
||||
],
|
||||
[
|
||||
{ name: 'Lithium', symbol: 'Li', number: 3, weight: 6.94, type: 'metal' },
|
||||
{ name: 'Beryllium', symbol: 'Be', number: 4, weight: 9.01, type: 'metal' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: 'Boron', symbol: 'B', number: 5, weight: 10.81, type: 'nonmetal' },
|
||||
{ name: 'Carbon', symbol: 'C', number: 6, weight: 12.01, type: 'nonmetal' },
|
||||
{ name: 'Nitrogen', symbol: 'N', number: 7, weight: 14.01, type: 'nonmetal' },
|
||||
{ name: 'Oxygen', symbol: 'O', number: 8, weight: 16, type: 'nonmetal' },
|
||||
{ name: 'Fluorine', symbol: 'F', number: 9, weight: 19, type: 'nonmetal' },
|
||||
{ name: 'Neon', symbol: 'Ne', number: 10, weight: 20.18, type: 'noblegas' },
|
||||
|
||||
|
||||
],
|
||||
[
|
||||
{ name: 'Sodium', symbol: 'Na', number: 11, weight: 22.99, type: 'metal' },
|
||||
{ name: 'Magnesium', symbol: 'Mg', number: 12, weight: 24.31, type: 'metal' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: 'Aluminum', symbol: 'Al', number: 13, weight: 26.98, type: 'metal' },
|
||||
{ name: 'Silicon', symbol: 'Si', number: 14, weight: 28.09, type: 'nonmetal' },
|
||||
{ name: 'Phosphorus', symbol: 'P', number: 15, weight: 30.97, type: 'nonmetal' },
|
||||
{ name: 'Sulfur', symbol: 'S', number: 16, weight: 32.07, type: 'nonmetal' },
|
||||
{ name: 'Chlorine', symbol: 'Cl', number: 17, weight: 35.45, type: 'nonmetal' },
|
||||
{ name: 'Argon', symbol: 'Ar', number: 18, weight: 39.95, type: 'noblegas' },
|
||||
],
|
||||
[
|
||||
{ name: 'Potassium', symbol: 'K', number: 19, weight: 39.098, type: 'metal' },
|
||||
{ name: 'Calcium', symbol: 'Ca', number: 20, weight: 40.078, type: 'metal' },
|
||||
{ name: 'Scandium', symbol: 'Sc', number: 21, weight: 44.956, type: 'metal' },
|
||||
{ name: 'Titanium', symbol: 'Ti', number: 22, weight: 47.87, type: 'metal' },
|
||||
{ name: 'Vanadium', symbol: 'V', number: 23, weight: 50.94, type: 'metal' },
|
||||
{ name: 'Chromium', symbol: 'Cr', number: 24, weight: 52, type: 'metal'/*, icon: 'chromium-browser'*/ },
|
||||
{ name: 'Manganese', symbol: 'Mn', number: 25, weight: 54.94, type: 'metal' },
|
||||
{ name: 'Iron', symbol: 'Fe', number: 26, weight: 55.85, type: 'metal' },
|
||||
{ name: 'Cobalt', symbol: 'Co', number: 27, weight: 58.93, type: 'metal' },
|
||||
{ name: 'Nickel', symbol: 'Ni', number: 28, weight: 58.69, type: 'metal' },
|
||||
{ name: 'Copper', symbol: 'Cu', number: 29, weight: 63.55, type: 'metal' },
|
||||
{ name: 'Zinc', symbol: 'Zn', number: 30, weight: 65.38, type: 'metal' },
|
||||
{ name: 'Gallium', symbol: 'Ga', number: 31, weight: 69.72, type: 'metal' },
|
||||
{ name: 'Germanium', symbol: 'Ge', number: 32, weight: 72.63, type: 'metal' },
|
||||
{ name: 'Arsenic', symbol: 'As', number: 33, weight: 74.92, type: 'nonmetal' },
|
||||
{ name: 'Selenium', symbol: 'Se', number: 34, weight: 78.96, type: 'nonmetal' },
|
||||
{ name: 'Bromine', symbol: 'Br', number: 35, weight: 79.904, type: 'nonmetal' },
|
||||
{ name: 'Krypton', symbol: 'Kr', number: 36, weight: 83.8, type: 'noblegas' },
|
||||
],
|
||||
[
|
||||
{ name: 'Rubidium', symbol: 'Rb', number: 37, weight: 85.47, type: 'metal' },
|
||||
{ name: 'Strontium', symbol: 'Sr', number: 38, weight: 87.62, type: 'metal' },
|
||||
{ name: 'Yttrium', symbol: 'Y', number: 39, weight: 88.91, type: 'metal' },
|
||||
{ name: 'Zirconium', symbol: 'Zr', number: 40, weight: 91.22, type: 'metal' },
|
||||
{ name: 'Niobium', symbol: 'Nb', number: 41, weight: 92.91, type: 'metal' },
|
||||
{ name: 'Molybdenum', symbol: 'Mo', number: 42, weight: 95.94, type: 'metal' },
|
||||
{ name: 'Technetium', symbol: 'Tc', number: 43, weight: 98, type: 'metal' },
|
||||
{ name: 'Ruthenium', symbol: 'Ru', number: 44, weight: 101.07, type: 'metal' },
|
||||
{ name: 'Rhodium', symbol: 'Rh', number: 45, weight: 102.91, type: 'metal' },
|
||||
{ name: 'Palladium', symbol: 'Pd', number: 46, weight: 106.42, type: 'metal' },
|
||||
{ name: 'Silver', symbol: 'Ag', number: 47, weight: 107.87, type: 'metal' },
|
||||
{ name: 'Cadmium', symbol: 'Cd', number: 48, weight: 112.41, type: 'metal' },
|
||||
{ name: 'Indium', symbol: 'In', number: 49, weight: 114.82, type: 'metal' },
|
||||
{ name: 'Tin', symbol: 'Sn', number: 50, weight: 118.71, type: 'metal' },
|
||||
{ name: 'Antimony', symbol: 'Sb', number: 51, weight: 121.76, type: 'metal' },
|
||||
{ name: 'Tellurium', symbol: 'Te', number: 52, weight: 127.6, type: 'nonmetal' },
|
||||
{ name: 'Iodine', symbol: 'I', number: 53, weight: 126.9, type: 'nonmetal' },
|
||||
{ name: 'Xenon', symbol: 'Xe', number: 54, weight: 131.29, type: 'noblegas' },
|
||||
],
|
||||
[
|
||||
{ name: 'Cesium', symbol: 'Cs', number: 55, weight: 132.91, type: 'metal' },
|
||||
{ name: 'Barium', symbol: 'Ba', number: 56, weight: 137.33, type: 'metal' },
|
||||
{ name: 'Lanthanum', symbol: 'La', number: 57, weight: 138.91, type: 'lanthanum' },
|
||||
{ name: 'Hafnium', symbol: 'Hf', number: 72, weight: 178.49, type: 'metal' },
|
||||
{ name: 'Tantalum', symbol: 'Ta', number: 73, weight: 180.95, type: 'metal' },
|
||||
{ name: 'Tungsten', symbol: 'W', number: 74, weight: 183.84, type: 'metal' },
|
||||
{ name: 'Rhenium', symbol: 'Re', number: 75, weight: 186.21, type: 'metal' },
|
||||
{ name: 'Osmium', symbol: 'Os', number: 76, weight: 190.23, type: 'metal' },
|
||||
{ name: 'Iridium', symbol: 'Ir', number: 77, weight: 192.22, type: 'metal' },
|
||||
{ name: 'Platinum', symbol: 'Pt', number: 78, weight: 195.09, type: 'metal' },
|
||||
{ name: 'Gold', symbol: 'Au', number: 79, weight: 196.97, type: 'metal' },
|
||||
{ name: 'Mercury', symbol: 'Hg', number: 80, weight: 200.59, type: 'metal' },
|
||||
{ name: 'Thallium', symbol: 'Tl', number: 81, weight: 204.38, type: 'metal' },
|
||||
{ name: 'Lead', symbol: 'Pb', number: 82, weight: 207.2, type: 'metal' },
|
||||
{ name: 'Bismuth', symbol: 'Bi', number: 83, weight: 208.98, type: 'metal' },
|
||||
{ name: 'Polonium', symbol: 'Po', number: 84, weight: 209, type: 'metal' },
|
||||
{ name: 'Astatine', symbol: 'At', number: 85, weight: 210, type: 'nonmetal' },
|
||||
{ name: 'Radon', symbol: 'Rn', number: 86, weight: 222, type: 'noblegas' },
|
||||
],
|
||||
[
|
||||
{ name: 'Francium', symbol: 'Fr', number: 87, weight: 223, type: 'metal' },
|
||||
{ name: 'Radium', symbol: 'Ra', number: 88, weight: 226, type: 'metal' },
|
||||
{ name: 'Actinium', symbol: 'Ac', number: 89, weight: 227, type: 'actinium' },
|
||||
{ name: 'Rutherfordium', symbol: 'Rf', number: 104, weight: 267, type: 'metal' },
|
||||
{ name: 'Dubnium', symbol: 'Db', number: 105, weight: 268, type: 'metal' },
|
||||
{ name: 'Seaborgium', symbol: 'Sg', number: 106, weight: 271, type: 'metal' },
|
||||
{ name: 'Bohrium', symbol: 'Bh', number: 107, weight: 272, type: 'metal' },
|
||||
{ name: 'Hassium', symbol: 'Hs', number: 108, weight: 277, type: 'metal' },
|
||||
{ name: 'Meitnerium', symbol: 'Mt', number: 109, weight: 278, type: 'metal' },
|
||||
{ name: 'Darmstadtium', symbol: 'Ds', number: 110, weight: 281, type: 'metal' },
|
||||
{ name: 'Roentgenium', symbol: 'Rg', number: 111, weight: 280, type: 'metal' },
|
||||
{ name: 'Copernicium', symbol: 'Cn', number: 112, weight: 285, type: 'metal' },
|
||||
{ name: 'Nihonium', symbol: 'Nh', number: 113, weight: 286, type: 'metal' },
|
||||
{ name: 'Flerovium', symbol: 'Fl', number: 114, weight: 289, type: 'metal' },
|
||||
{ name: 'Moscovium', symbol: 'Mc', number: 115, weight: 290, type: 'metal' },
|
||||
{ name: 'Livermorium', symbol: 'Lv', number: 116, weight: 293, type: 'metal' },
|
||||
{ name: 'Tennessine', symbol: 'Ts', number: 117, weight: 294, type: 'metal' },
|
||||
{ name: 'Oganesson', symbol: 'Og', number: 118, weight: 294, type: 'noblegas' },
|
||||
],
|
||||
]
|
||||
|
||||
const series = [
|
||||
[
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: 'Cerium', symbol: 'Ce', number: 58, weight: 140.12, type: 'lanthanum' },
|
||||
{ name: 'Praseodymium', symbol: 'Pr', number: 59, weight: 140.91, type: 'lanthanum' },
|
||||
{ name: 'Neodymium', symbol: 'Nd', number: 60, weight: 144.24, type: 'lanthanum' },
|
||||
{ name: 'Promethium', symbol: 'Pm', number: 61, weight: 145, type: 'lanthanum' },
|
||||
{ name: 'Samarium', symbol: 'Sm', number: 62, weight: 150.36, type: 'lanthanum' },
|
||||
{ name: 'Europium', symbol: 'Eu', number: 63, weight: 151.96, type: 'lanthanum' },
|
||||
{ name: 'Gadolinium', symbol: 'Gd', number: 64, weight: 157.25, type: 'lanthanum' },
|
||||
{ name: 'Terbium', symbol: 'Tb', number: 65, weight: 158.93, type: 'lanthanum' },
|
||||
{ name: 'Dysprosium', symbol: 'Dy', number: 66, weight: 162.5, type: 'lanthanum' },
|
||||
{ name: 'Holmium', symbol: 'Ho', number: 67, weight: 164.93, type: 'lanthanum' },
|
||||
{ name: 'Erbium', symbol: 'Er', number: 68, weight: 167.26, type: 'lanthanum' },
|
||||
{ name: 'Thulium', symbol: 'Tm', number: 69, weight: 168.93, type: 'lanthanum' },
|
||||
{ name: 'Ytterbium', symbol: 'Yb', number: 70, weight: 173.04, type: 'lanthanum' },
|
||||
{ name: 'Lutetium', symbol: 'Lu', number: 71, weight: 174.97, type: 'lanthanum' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
],
|
||||
[
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
{ name: 'Thorium', symbol: 'Th', number: 90, weight: 232.04, type: 'actinium' },
|
||||
{ name: 'Protactinium', symbol: 'Pa', number: 91, weight: 231.04, type: 'actinium' },
|
||||
{ name: 'Uranium', symbol: 'U', number: 92, weight: 238.03, type: 'actinium' },
|
||||
{ name: 'Neptunium', symbol: 'Np', number: 93, weight: 237, type: 'actinium' },
|
||||
{ name: 'Plutonium', symbol: 'Pu', number: 94, weight: 244, type: 'actinium' },
|
||||
{ name: 'Americium', symbol: 'Am', number: 95, weight: 243, type: 'actinium' },
|
||||
{ name: 'Curium', symbol: 'Cm', number: 96, weight: 247, type: 'actinium' },
|
||||
{ name: 'Berkelium', symbol: 'Bk', number: 97, weight: 247, type: 'actinium' },
|
||||
{ name: 'Californium', symbol: 'Cf', number: 98, weight: 251, type: 'actinium' },
|
||||
{ name: 'Einsteinium', symbol: 'Es', number: 99, weight: 252, type: 'actinium' },
|
||||
{ name: 'Fermium', symbol: 'Fm', number: 100, weight: 257, type: 'actinium' },
|
||||
{ name: 'Mendelevium', symbol: 'Md', number: 101, weight: 258, type: 'actinium' },
|
||||
{ name: 'Nobelium', symbol: 'No', number: 102, weight: 259, type: 'actinium' },
|
||||
{ name: 'Lawrencium', symbol: 'Lr', number: 103, weight: 262, type: 'actinium' },
|
||||
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
|
||||
],
|
||||
];
|
||||
|
||||
const niceTypes = {
|
||||
'metal': "Metal",
|
||||
'nonmetal': "Nonmetal",
|
||||
'noblegas': "Noble gas",
|
||||
'lanthanum': "Lanthanum",
|
||||
'actinium': "Actinium"
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope { // Scope
|
||||
id: root
|
||||
property bool pinned: Config.options?.dock.pinnedOnStartup ?? false
|
||||
|
||||
Variants {
|
||||
// For each monitor
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: dockRoot
|
||||
// Window
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
visible: !GlobalStates.screenLocked
|
||||
|
||||
property bool reveal: root.pinned || (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse) || dockApps.requestDockShow || (!ToplevelManager.activeToplevel?.activated)
|
||||
|
||||
anchors {
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
exclusiveZone: root.pinned ? implicitHeight - (Appearance.sizes.hyprlandGapsOut) - (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0
|
||||
|
||||
implicitWidth: dockBackground.implicitWidth
|
||||
WlrLayershell.namespace: "quickshell:dock"
|
||||
color: "transparent"
|
||||
|
||||
implicitHeight: (Config.options?.dock.height ?? 70) + Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut
|
||||
|
||||
mask: Region {
|
||||
item: dockMouseArea
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dockMouseArea
|
||||
height: parent.height
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: dockRoot.reveal ? 0 : Config.options?.dock.hoverToReveal ? (dockRoot.implicitHeight - Config.options.dock.hoverRegionHeight) : (dockRoot.implicitHeight + 1)
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
implicitWidth: dockHoverRegion.implicitWidth + Appearance.sizes.elevationMargin * 2
|
||||
hoverEnabled: true
|
||||
|
||||
Behavior on anchors.topMargin {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dockHoverRegion
|
||||
anchors.fill: parent
|
||||
implicitWidth: dockBackground.implicitWidth
|
||||
|
||||
Item { // Wrapper for the dock background
|
||||
id: dockBackground
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
implicitWidth: dockRow.implicitWidth + 5 * 2
|
||||
height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: dockVisualBackground
|
||||
}
|
||||
Rectangle { // The real rectangle that is visible
|
||||
id: dockVisualBackground
|
||||
property real margin: Appearance.sizes.elevationMargin
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Appearance.sizes.elevationMargin
|
||||
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.large
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: dockRow
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 3
|
||||
property real padding: 5
|
||||
|
||||
VerticalButtonGroup {
|
||||
Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
|
||||
GroupButton {
|
||||
// Pin button
|
||||
baseWidth: 35
|
||||
baseHeight: 35
|
||||
clickedWidth: baseWidth
|
||||
clickedHeight: baseHeight + 20
|
||||
buttonRadius: Appearance.rounding.normal
|
||||
toggled: root.pinned
|
||||
onClicked: root.pinned = !root.pinned
|
||||
contentItem: MaterialSymbol {
|
||||
text: "keep"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
|
||||
}
|
||||
}
|
||||
}
|
||||
DockSeparator {}
|
||||
DockApps {
|
||||
id: dockApps
|
||||
buttonPadding: dockRow.padding
|
||||
}
|
||||
DockSeparator {}
|
||||
DockButton {
|
||||
Layout.fillHeight: true
|
||||
onClicked: GlobalStates.overviewOpen = !GlobalStates.overviewOpen
|
||||
topInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
|
||||
bottomInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: parent.width / 2
|
||||
text: "apps"
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
|
||||
DockButton {
|
||||
id: root
|
||||
property var appToplevel
|
||||
property var appListRoot
|
||||
property int lastFocused: -1
|
||||
property real iconSize: 35
|
||||
property real countDotWidth: 10
|
||||
property real countDotHeight: 4
|
||||
property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined
|
||||
|
||||
readonly property bool isSeparator: appToplevel.appId === "SEPARATOR"
|
||||
readonly property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId)
|
||||
enabled: !isSeparator
|
||||
implicitWidth: isSeparator ? 1 : implicitHeight - topInset - bottomInset
|
||||
|
||||
Loader {
|
||||
active: isSeparator
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: dockVisualBackground.margin + dockRow.padding + Appearance.rounding.normal
|
||||
bottomMargin: dockVisualBackground.margin + dockRow.padding + Appearance.rounding.normal
|
||||
}
|
||||
sourceComponent: DockSeparator {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: appToplevel.toplevels.length > 0
|
||||
sourceComponent: MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
onEntered: {
|
||||
appListRoot.lastHoveredButton = root
|
||||
appListRoot.buttonHovered = true
|
||||
lastFocused = appToplevel.toplevels.length - 1
|
||||
}
|
||||
onExited: {
|
||||
if (appListRoot.lastHoveredButton === root) {
|
||||
appListRoot.buttonHovered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (appToplevel.toplevels.length === 0) {
|
||||
root.desktopEntry?.execute();
|
||||
return;
|
||||
}
|
||||
lastFocused = (lastFocused + 1) % appToplevel.toplevels.length
|
||||
appToplevel.toplevels[lastFocused].activate()
|
||||
}
|
||||
|
||||
middleClickAction: () => {
|
||||
root.desktopEntry?.execute();
|
||||
}
|
||||
|
||||
altAction: () => {
|
||||
if (Config.options.dock.pinnedApps.indexOf(appToplevel.appId) !== -1) {
|
||||
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.filter(id => id !== appToplevel.appId)
|
||||
} else {
|
||||
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.concat([appToplevel.appId])
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Loader {
|
||||
active: !isSeparator
|
||||
sourceComponent: Item {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Loader {
|
||||
id: iconImageLoader
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
active: !root.isSeparator
|
||||
sourceComponent: IconImage {
|
||||
source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing")
|
||||
implicitSize: root.iconSize
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.dock.monochromeIcons
|
||||
anchors.fill: iconImageLoader
|
||||
sourceComponent: Item {
|
||||
Desaturate {
|
||||
id: desaturatedIcon
|
||||
visible: false // There's already color overlay
|
||||
anchors.fill: parent
|
||||
source: iconImageLoader
|
||||
desaturation: 0.8
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: desaturatedIcon
|
||||
source: desaturatedIcon
|
||||
color: ColorUtils.transparentize(Appearance.colors.colPrimary, 0.9)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 3
|
||||
anchors {
|
||||
top: iconImageLoader.bottom
|
||||
topMargin: 2
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
Repeater {
|
||||
model: Math.min(appToplevel.toplevels.length, 3)
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: (appToplevel.toplevels.length <= 3) ?
|
||||
root.countDotWidth : root.countDotHeight // Circles when too many
|
||||
implicitHeight: root.countDotHeight
|
||||
color: appIsActive ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real maxWindowPreviewHeight: 200
|
||||
property real maxWindowPreviewWidth: 300
|
||||
property real windowControlsHeight: 30
|
||||
property real buttonPadding: 5
|
||||
|
||||
property Item lastHoveredButton
|
||||
property bool buttonHovered: false
|
||||
property bool requestDockShow: previewPopup.show
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
|
||||
implicitWidth: listView.implicitWidth
|
||||
|
||||
StyledListView {
|
||||
id: listView
|
||||
spacing: 2
|
||||
orientation: ListView.Horizontal
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
implicitWidth: contentWidth
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
objectProp: "appId"
|
||||
values: TaskbarApps.apps
|
||||
}
|
||||
delegate: DockAppButton {
|
||||
required property var modelData
|
||||
appToplevel: modelData
|
||||
appListRoot: root
|
||||
|
||||
topInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding
|
||||
bottomInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding
|
||||
}
|
||||
}
|
||||
|
||||
PopupWindow {
|
||||
id: previewPopup
|
||||
property var appTopLevel: root.lastHoveredButton?.appToplevel
|
||||
property bool allPreviewsReady: false
|
||||
Connections {
|
||||
target: root
|
||||
function onLastHoveredButtonChanged() {
|
||||
previewPopup.allPreviewsReady = false; // Reset readiness when the hovered button changes
|
||||
}
|
||||
}
|
||||
function updatePreviewReadiness() {
|
||||
for(var i = 0; i < previewRowLayout.children.length; i++) {
|
||||
const view = previewRowLayout.children[i];
|
||||
if (view.hasContent === false) {
|
||||
allPreviewsReady = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
allPreviewsReady = true;
|
||||
}
|
||||
property bool shouldShow: {
|
||||
const hoverConditions = (popupMouseArea.containsMouse || root.buttonHovered)
|
||||
return hoverConditions && allPreviewsReady;
|
||||
}
|
||||
property bool show: false
|
||||
|
||||
onShouldShowChanged: {
|
||||
if (shouldShow) {
|
||||
// show = true;
|
||||
updateTimer.restart();
|
||||
} else {
|
||||
updateTimer.restart();
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
previewPopup.show = previewPopup.shouldShow
|
||||
}
|
||||
}
|
||||
anchor {
|
||||
window: root.QsWindow.window
|
||||
adjustment: PopupAdjustment.None
|
||||
gravity: Edges.Top | Edges.Right
|
||||
edges: Edges.Top | Edges.Left
|
||||
|
||||
}
|
||||
visible: popupBackground.visible
|
||||
color: "transparent"
|
||||
implicitWidth: root.QsWindow.window?.width ?? 1
|
||||
implicitHeight: popupMouseArea.implicitHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2
|
||||
|
||||
MouseArea {
|
||||
id: popupMouseArea
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
|
||||
implicitHeight: root.maxWindowPreviewHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2
|
||||
hoverEnabled: true
|
||||
x: {
|
||||
const itemCenter = root.QsWindow?.mapFromItem(root.lastHoveredButton, root.lastHoveredButton?.width / 2, 0);
|
||||
return itemCenter.x - width / 2
|
||||
}
|
||||
StyledRectangularShadow {
|
||||
target: popupBackground
|
||||
opacity: previewPopup.show ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: popupBackground
|
||||
property real padding: 5
|
||||
opacity: previewPopup.show ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
clip: true
|
||||
color: Appearance.colors.colSurfaceContainer
|
||||
radius: Appearance.rounding.normal
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Appearance.sizes.elevationMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitHeight: previewRowLayout.implicitHeight + padding * 2
|
||||
implicitWidth: previewRowLayout.implicitWidth + padding * 2
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: previewRowLayout
|
||||
anchors.centerIn: parent
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: previewPopup.appTopLevel?.toplevels ?? []
|
||||
}
|
||||
RippleButton {
|
||||
id: windowButton
|
||||
required property var modelData
|
||||
padding: 0
|
||||
middleClickAction: () => {
|
||||
windowButton.modelData?.close();
|
||||
}
|
||||
onClicked: {
|
||||
windowButton.modelData?.activate();
|
||||
}
|
||||
contentItem: ColumnLayout {
|
||||
implicitWidth: screencopyView.implicitWidth
|
||||
implicitHeight: screencopyView.implicitHeight
|
||||
|
||||
ButtonGroup {
|
||||
contentWidth: parent.width - anchors.margins * 2
|
||||
WrapperRectangle {
|
||||
Layout.fillWidth: true
|
||||
color: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer)
|
||||
radius: Appearance.rounding.small
|
||||
margin: 5
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
text: windowButton.modelData?.title
|
||||
elide: Text.ElideRight
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
GroupButton {
|
||||
id: closeButton
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer)
|
||||
baseWidth: windowControlsHeight
|
||||
baseHeight: windowControlsHeight
|
||||
buttonRadius: Appearance.rounding.full
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "close"
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
onClicked: {
|
||||
windowButton.modelData?.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
ScreencopyView {
|
||||
id: screencopyView
|
||||
captureSource: previewPopup ? windowButton.modelData : null
|
||||
live: true
|
||||
paintCursor: true
|
||||
constraintSize: Qt.size(root.maxWindowPreviewWidth, root.maxWindowPreviewHeight)
|
||||
onHasContentChanged: {
|
||||
previewPopup.updatePreviewReadiness();
|
||||
}
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: screencopyView.width
|
||||
height: screencopyView.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RippleButton {
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
|
||||
implicitWidth: implicitHeight - topInset - bottomInset
|
||||
buttonRadius: Appearance.rounding.normal
|
||||
|
||||
background.implicitHeight: 50
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
Layout.topMargin: Appearance.sizes.elevationMargin + dockRow.padding + Appearance.rounding.normal
|
||||
Layout.bottomMargin: Appearance.sizes.hyprlandGapsOut + dockRow.padding + Appearance.rounding.normal
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 1
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Process {
|
||||
id: unlockKeyringProc
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
KeyringStorage.fetchKeyringData();
|
||||
}
|
||||
}
|
||||
function unlockKeyring() {
|
||||
unlockKeyringProc.exec({
|
||||
environment: ({
|
||||
"UNLOCK_PASSWORD": lockContext.currentText
|
||||
}),
|
||||
command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")]
|
||||
})
|
||||
}
|
||||
|
||||
property var windowData: []
|
||||
function saveWindowPositionAndTile() {
|
||||
Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "true"])
|
||||
root.windowData = HyprlandData.windowList.filter(w => (w.floating && w.workspace.id === HyprlandData.activeWorkspace.id))
|
||||
root.windowData.forEach(w => {
|
||||
Hyprland.dispatch(`pseudo address:${w.address}`)
|
||||
Hyprland.dispatch(`settiled address:${w.address}`)
|
||||
Hyprland.dispatch(`movetoworkspacesilent ${w.workspace.id},address:${w.address}`)
|
||||
})
|
||||
}
|
||||
function restoreWindowPositionAndTile() {
|
||||
root.windowData.forEach(w => {
|
||||
Hyprland.dispatch(`setfloating address:${w.address}`)
|
||||
Hyprland.dispatch(`movewindowpixel exact ${w.at[0]} ${w.at[1]}, address:${w.address}`)
|
||||
Hyprland.dispatch(`pseudo address:${w.address}`)
|
||||
})
|
||||
Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "false"])
|
||||
}
|
||||
|
||||
// This stores all the information shared between the lock surfaces on each screen.
|
||||
// https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen
|
||||
LockContext {
|
||||
id: lockContext
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenLockedChanged() {
|
||||
if (GlobalStates.screenLocked) {
|
||||
lockContext.reset();
|
||||
lockContext.tryFingerUnlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnlocked: (targetAction) => {
|
||||
// Perform the target action if it's not just unlocking
|
||||
if (targetAction == LockContext.ActionEnum.Poweroff) {
|
||||
Session.poweroff();
|
||||
return;
|
||||
} else if (targetAction == LockContext.ActionEnum.Reboot) {
|
||||
Session.reboot();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock the keyring if configured to do so
|
||||
if (Config.options.lock.security.unlockKeyring) root.unlockKeyring();
|
||||
|
||||
// Unlock the screen before exiting, or the compositor will display a
|
||||
// fallback lock you can't interact with.
|
||||
GlobalStates.screenLocked = false;
|
||||
|
||||
// Refocus last focused window on unlock (hack)
|
||||
Quickshell.execDetached(["bash", "-c", `sleep 0.2; hyprctl --batch "dispatch togglespecialworkspace; dispatch togglespecialworkspace"`])
|
||||
|
||||
// Reset
|
||||
lockContext.reset();
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
locked: GlobalStates.screenLocked
|
||||
|
||||
WlSessionLockSurface {
|
||||
color: "transparent"
|
||||
Loader {
|
||||
active: GlobalStates.screenLocked
|
||||
anchors.fill: parent
|
||||
opacity: active ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
sourceComponent: LockSurface {
|
||||
context: lockContext
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blur layer hack
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: Scope {
|
||||
required property ShellScreen modelData
|
||||
property bool shouldPush: GlobalStates.screenLocked
|
||||
property string targetMonitorName: modelData.name
|
||||
property int verticalMovementDistance: modelData.height
|
||||
property int horizontalSqueeze: modelData.width * 0.2
|
||||
onShouldPushChanged: {
|
||||
if (shouldPush) {
|
||||
root.saveWindowPositionAndTile();
|
||||
Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, ${verticalMovementDistance}, ${-verticalMovementDistance}, ${horizontalSqueeze}, ${horizontalSqueeze}`])
|
||||
} else {
|
||||
Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, 0, 0, 0, 0`])
|
||||
root.restoreWindowPositionAndTile();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lock() {
|
||||
if (Config.options.lock.useHyprlock) {
|
||||
Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]);
|
||||
return;
|
||||
}
|
||||
GlobalStates.screenLocked = true;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "lock"
|
||||
|
||||
function activate(): void {
|
||||
root.lock();
|
||||
}
|
||||
function focus(): void {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lock"
|
||||
description: "Locks the screen"
|
||||
|
||||
onPressed: {
|
||||
root.lock()
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lockFocus"
|
||||
description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason"
|
||||
+ "decides to keyboard-unfocus the lock screen"
|
||||
|
||||
onPressed: {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
function initIfReady() {
|
||||
if (!Config.ready || !Persistent.ready) return;
|
||||
if (Config.options.lock.launchOnStartup && Persistent.isNewHyprlandInstance) {
|
||||
root.lock();
|
||||
} else {
|
||||
KeyringStorage.fetchKeyringData();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Config
|
||||
function onReadyChanged() {
|
||||
root.initIfReady();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Persistent
|
||||
function onReadyChanged() {
|
||||
root.initIfReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pam
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
enum ActionEnum { Unlock, Poweroff, Reboot }
|
||||
|
||||
signal shouldReFocus()
|
||||
signal unlocked(targetAction: var)
|
||||
signal failed()
|
||||
|
||||
// These properties are in the context and not individual lock surfaces
|
||||
// so all surfaces can share the same state.
|
||||
property string currentText: ""
|
||||
property bool unlockInProgress: false
|
||||
property bool showFailure: false
|
||||
property bool fingerprintsConfigured: false
|
||||
property var targetAction: LockContext.ActionEnum.Unlock
|
||||
|
||||
function resetTargetAction() {
|
||||
root.targetAction = LockContext.ActionEnum.Unlock;
|
||||
}
|
||||
|
||||
function clearText() {
|
||||
root.currentText = "";
|
||||
}
|
||||
|
||||
function resetClearTimer() {
|
||||
passwordClearTimer.restart();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
root.resetTargetAction();
|
||||
root.clearText();
|
||||
root.unlockInProgress = false;
|
||||
stopFingerPam();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: passwordClearTimer
|
||||
interval: 10000
|
||||
onTriggered: {
|
||||
root.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentTextChanged: {
|
||||
if (currentText.length > 0) {
|
||||
showFailure = false;
|
||||
GlobalStates.screenUnlockFailed = false;
|
||||
}
|
||||
GlobalStates.screenLockContainsCharacters = currentText.length > 0;
|
||||
passwordClearTimer.restart();
|
||||
}
|
||||
|
||||
function tryUnlock() {
|
||||
root.unlockInProgress = true;
|
||||
pam.start();
|
||||
}
|
||||
|
||||
function tryFingerUnlock() {
|
||||
if (root.fingerprintsConfigured) {
|
||||
fingerPam.start();
|
||||
}
|
||||
}
|
||||
|
||||
function stopFingerPam() {
|
||||
if (fingerPam.running) {
|
||||
fingerPam.abort();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fingerprintCheckProc
|
||||
running: true
|
||||
command: ["bash", "-c", "fprintd-list $(whoami)"]
|
||||
stdout: StdioCollector {
|
||||
id: fingerprintOutputCollector
|
||||
onStreamFinished: {
|
||||
root.fingerprintsConfigured = fingerprintOutputCollector.text.includes("Fingerprints for user");
|
||||
}
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0) {
|
||||
// console.warn("[LockContext] fprintd-list command exited with error:", exitCode, exitStatus);
|
||||
root.fingerprintsConfigured = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
|
||||
// pam_unix will ask for a response for the password prompt
|
||||
onPamMessage: {
|
||||
if (this.responseRequired) {
|
||||
this.respond(root.currentText);
|
||||
}
|
||||
}
|
||||
|
||||
// pam_unix won't send any important messages so all we need is the completion status.
|
||||
onCompleted: result => {
|
||||
if (result == PamResult.Success) {
|
||||
root.unlocked(root.targetAction);
|
||||
stopFingerPam();
|
||||
} else {
|
||||
root.clearText();
|
||||
root.unlockInProgress = false;
|
||||
GlobalStates.screenUnlockFailed = true;
|
||||
root.showFailure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: fingerPam
|
||||
|
||||
configDirectory: "pam"
|
||||
config: "fprintd.conf"
|
||||
|
||||
onCompleted: result => {
|
||||
if (result == PamResult.Success) {
|
||||
root.unlocked(root.targetAction);
|
||||
stopFingerPam();
|
||||
} else if (result == PamResult.Error) { // if timeout or etc..
|
||||
tryFingerUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Services.UPower
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.ii.bar as Bar
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
required property LockContext context
|
||||
property bool active: false
|
||||
property bool showInputField: active || context.currentText.length > 0
|
||||
readonly property bool requirePasswordToPower: Config.options.lock.security.requirePasswordToPower
|
||||
|
||||
// Force focus on entry
|
||||
function forceFieldFocus() {
|
||||
passwordBox.forceActiveFocus();
|
||||
}
|
||||
Connections {
|
||||
target: context
|
||||
function onShouldReFocus() {
|
||||
forceFieldFocus();
|
||||
}
|
||||
}
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressed: mouse => {
|
||||
forceFieldFocus();
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
forceFieldFocus();
|
||||
}
|
||||
|
||||
// Toolbar appearing animation
|
||||
property real toolbarScale: 0.9
|
||||
property real toolbarOpacity: 0
|
||||
Behavior on toolbarScale {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
Behavior on toolbarOpacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
// Init
|
||||
Component.onCompleted: {
|
||||
forceFieldFocus();
|
||||
toolbarScale = 1;
|
||||
toolbarOpacity = 1;
|
||||
}
|
||||
|
||||
// Key presses
|
||||
Keys.onPressed: event => {
|
||||
root.context.resetClearTimer();
|
||||
if (event.key === Qt.Key_Escape) { // Esc to clear
|
||||
root.context.currentText = "";
|
||||
}
|
||||
forceFieldFocus();
|
||||
}
|
||||
|
||||
// RippleButton {
|
||||
// anchors {
|
||||
// top: parent.top
|
||||
// left: parent.left
|
||||
// leftMargin: 10
|
||||
// topMargin: 10
|
||||
// }
|
||||
// implicitHeight: 40
|
||||
// colBackground: Appearance.colors.colLayer2
|
||||
// onClicked: {
|
||||
// context.unlocked(LockContext.ActionEnum.Unlock);
|
||||
// GlobalStates.screenLocked = false;
|
||||
// }
|
||||
// contentItem: StyledText {
|
||||
// text: "[[ DEBUG BYPASS ]]"
|
||||
// }
|
||||
// }
|
||||
|
||||
// Main toolbar: password box
|
||||
Toolbar {
|
||||
id: mainIsland
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 20
|
||||
}
|
||||
Behavior on anchors.bottomMargin {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
scale: root.toolbarScale
|
||||
opacity: root.toolbarOpacity
|
||||
|
||||
// Fingerprint
|
||||
Loader {
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 6
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
active: root.context.fingerprintsConfigured
|
||||
visible: active
|
||||
|
||||
sourceComponent: MaterialSymbol {
|
||||
id: fingerprintIcon
|
||||
fill: 1
|
||||
text: "fingerprint"
|
||||
iconSize: Appearance.font.pixelSize.hugeass
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarTextField {
|
||||
id: passwordBox
|
||||
Layout.rightMargin: -Layout.leftMargin
|
||||
placeholderText: GlobalStates.screenUnlockFailed ? Translation.tr("Incorrect password") : Translation.tr("Enter password")
|
||||
|
||||
// Style
|
||||
clip: true
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
|
||||
// Password
|
||||
enabled: !root.context.unlockInProgress
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
|
||||
// Synchronizing (across monitors) and unlocking
|
||||
onTextChanged: root.context.currentText = this.text
|
||||
onAccepted: root.context.tryUnlock()
|
||||
Connections {
|
||||
target: root.context
|
||||
function onCurrentTextChanged() {
|
||||
passwordBox.text = root.context.currentText;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
root.context.resetClearTimer();
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: passwordBox.width - 8
|
||||
height: passwordBox.height
|
||||
radius: height / 2
|
||||
}
|
||||
}
|
||||
|
||||
// Shake when wrong password
|
||||
SequentialAnimation {
|
||||
id: wrongPasswordShakeAnim
|
||||
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: -30; duration: 50 }
|
||||
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: 30; duration: 50 }
|
||||
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: -15; duration: 40 }
|
||||
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: 15; duration: 40 }
|
||||
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: 0; duration: 30 }
|
||||
}
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenUnlockFailedChanged() {
|
||||
if (GlobalStates.screenUnlockFailed) wrongPasswordShakeAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// We're drawing dots manually
|
||||
property bool materialShapeChars: Config.options.lock.materialShapeChars
|
||||
color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, materialShapeChars ? 1 : 0)
|
||||
Loader {
|
||||
active: passwordBox.materialShapeChars
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: passwordBox.padding
|
||||
rightMargin: passwordBox.padding
|
||||
}
|
||||
sourceComponent: PasswordChars {
|
||||
length: root.context.currentText.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
id: confirmButton
|
||||
implicitWidth: height
|
||||
toggled: true
|
||||
enabled: !root.context.unlockInProgress
|
||||
colBackgroundToggled: Appearance.colors.colPrimary
|
||||
|
||||
onClicked: root.context.tryUnlock()
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 24
|
||||
text: {
|
||||
if (root.context.targetAction === LockContext.ActionEnum.Unlock) {
|
||||
return "arrow_right_alt";
|
||||
} else if (root.context.targetAction === LockContext.ActionEnum.Poweroff) {
|
||||
return "power_settings_new";
|
||||
} else if (root.context.targetAction === LockContext.ActionEnum.Reboot) {
|
||||
return "restart_alt";
|
||||
}
|
||||
}
|
||||
color: confirmButton.enabled ? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Left toolbar
|
||||
Toolbar {
|
||||
id: leftIsland
|
||||
anchors {
|
||||
right: mainIsland.left
|
||||
top: mainIsland.top
|
||||
bottom: mainIsland.bottom
|
||||
rightMargin: 10
|
||||
}
|
||||
scale: root.toolbarScale
|
||||
opacity: root.toolbarOpacity
|
||||
|
||||
// Username
|
||||
IconAndTextPair {
|
||||
Layout.leftMargin: 8
|
||||
icon: "account_circle"
|
||||
text: SystemInfo.username
|
||||
}
|
||||
|
||||
// Keyboard layout (Xkb)
|
||||
Loader {
|
||||
Layout.rightMargin: 8
|
||||
Layout.fillHeight: true
|
||||
|
||||
active: true
|
||||
visible: active
|
||||
|
||||
sourceComponent: Row {
|
||||
spacing: 8
|
||||
|
||||
MaterialSymbol {
|
||||
id: keyboardIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fill: 1
|
||||
text: "keyboard_alt"
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceComponent: StyledText {
|
||||
text: HyprlandXkb.currentLayoutCode
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
animateChange: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard layout (Fcitx)
|
||||
Bar.SysTray {
|
||||
Layout.rightMargin: 10
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
showSeparator: false
|
||||
showOverflowMenu: false
|
||||
pinnedItems: SystemTray.items.values.filter(i => i.id == "Fcitx")
|
||||
visible: pinnedItems.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Right toolbar
|
||||
Toolbar {
|
||||
id: rightIsland
|
||||
anchors {
|
||||
left: mainIsland.right
|
||||
top: mainIsland.top
|
||||
bottom: mainIsland.bottom
|
||||
leftMargin: 10
|
||||
}
|
||||
|
||||
scale: root.toolbarScale
|
||||
opacity: root.toolbarOpacity
|
||||
|
||||
IconAndTextPair {
|
||||
visible: Battery.available
|
||||
icon: Battery.isCharging ? "bolt" : "battery_android_full"
|
||||
text: Math.round(Battery.percentage * 100)
|
||||
color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
|
||||
IconToolbarButton {
|
||||
id: sleepButton
|
||||
onClicked: Session.suspend()
|
||||
text: "dark_mode"
|
||||
}
|
||||
|
||||
PasswordGuardedIconToolbarButton {
|
||||
id: powerButton
|
||||
text: "power_settings_new"
|
||||
targetAction: LockContext.ActionEnum.Poweroff
|
||||
}
|
||||
|
||||
PasswordGuardedIconToolbarButton {
|
||||
id: rebootButton
|
||||
text: "restart_alt"
|
||||
targetAction: LockContext.ActionEnum.Reboot
|
||||
}
|
||||
}
|
||||
|
||||
component PasswordGuardedIconToolbarButton: IconToolbarButton {
|
||||
id: guardedBtn
|
||||
required property var targetAction
|
||||
|
||||
toggled: root.context.targetAction === guardedBtn.targetAction
|
||||
|
||||
onClicked: {
|
||||
if (!root.requirePasswordToPower) {
|
||||
root.context.unlocked(guardedBtn.targetAction);
|
||||
return;
|
||||
}
|
||||
if (root.context.targetAction === guardedBtn.targetAction) {
|
||||
root.context.resetTargetAction();
|
||||
} else {
|
||||
root.context.targetAction = guardedBtn.targetAction;
|
||||
root.context.shouldReFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component IconAndTextPair: Row {
|
||||
id: pair
|
||||
required property string icon
|
||||
required property string text
|
||||
property color color: Appearance.colors.colOnSurfaceVariant
|
||||
|
||||
spacing: 4
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fill: 1
|
||||
text: pair.icon
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
animateChange: true
|
||||
color: pair.color
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: pair.text
|
||||
color: pair.color
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import Quickshell
|
||||
|
||||
StyledFlickable {
|
||||
id: root
|
||||
required property int length
|
||||
contentWidth: dotsRow.implicitWidth
|
||||
contentX: (Math.max(contentWidth - width, 0))
|
||||
Behavior on contentX {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Row {
|
||||
id: dotsRow
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: 4
|
||||
}
|
||||
spacing: 10
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Array(root.length)
|
||||
}
|
||||
delegate: Item {
|
||||
id: charItem
|
||||
required property int index
|
||||
implicitWidth: 10
|
||||
implicitHeight: 10
|
||||
MaterialShape {
|
||||
id: materialShape
|
||||
anchors.centerIn: parent
|
||||
property list<var> charShapes: [
|
||||
MaterialShape.Shape.Clover4Leaf,
|
||||
MaterialShape.Shape.Arrow,
|
||||
MaterialShape.Shape.Pill,
|
||||
MaterialShape.Shape.SoftBurst,
|
||||
MaterialShape.Shape.Diamond,
|
||||
MaterialShape.Shape.ClamShell,
|
||||
MaterialShape.Shape.Pentagon,
|
||||
]
|
||||
shape: charShapes[charItem.index % charShapes.length]
|
||||
// Animate on appearance
|
||||
color: Appearance.colors.colPrimary
|
||||
implicitSize: 0
|
||||
opacity: 0
|
||||
scale: 0.5
|
||||
Component.onCompleted: {
|
||||
appearAnim.start();
|
||||
}
|
||||
ParallelAnimation {
|
||||
id: appearAnim
|
||||
NumberAnimation {
|
||||
target: materialShape
|
||||
properties: "opacity"
|
||||
to: 1
|
||||
duration: 50
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
NumberAnimation {
|
||||
target: materialShape
|
||||
properties: "scale"
|
||||
to: 1
|
||||
duration: 200
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
NumberAnimation {
|
||||
target: materialShape
|
||||
properties: "implicitSize"
|
||||
to: 18
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
ColorAnimation {
|
||||
target: materialShape
|
||||
properties: "color"
|
||||
from: Appearance.colors.colPrimary
|
||||
to: Appearance.colors.colOnLayer1
|
||||
duration: 1000
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
auth sufficient pam_fprintd.so
|
||||
@@ -0,0 +1,249 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
property bool visible: false
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
readonly property var realPlayers: Mpris.players.values.filter(player => isRealPlayer(player))
|
||||
readonly property var meaningfulPlayers: filterDuplicatePlayers(realPlayers)
|
||||
readonly property real osdWidth: Appearance.sizes.osdWidth
|
||||
readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth
|
||||
readonly property real widgetHeight: Appearance.sizes.mediaControlsHeight
|
||||
property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
property list<real> visualizerPoints: []
|
||||
|
||||
property bool hasPlasmaIntegration: false
|
||||
Process {
|
||||
id: plasmaIntegrationAvailabilityCheckProc
|
||||
running: true
|
||||
command: ["bash", "-c", "command -v plasma-browser-integration-host"]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.hasPlasmaIntegration = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
function isRealPlayer(player) {
|
||||
if (!Config.options.media.filterDuplicatePlayers) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
// Remove unecessary native buses from browsers if there's plasma integration
|
||||
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
|
||||
// playerctld just copies other buses and we don't need duplicates
|
||||
!player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') &&
|
||||
// Non-instance mpd bus
|
||||
!(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd')));
|
||||
}
|
||||
function filterDuplicatePlayers(players) {
|
||||
let filtered = [];
|
||||
let used = new Set();
|
||||
|
||||
for (let i = 0; i < players.length; ++i) {
|
||||
if (used.has(i))
|
||||
continue;
|
||||
let p1 = players[i];
|
||||
let group = [i];
|
||||
|
||||
// Find duplicates by trackTitle prefix
|
||||
for (let j = i + 1; j < players.length; ++j) {
|
||||
let p2 = players[j];
|
||||
if (p1.trackTitle && p2.trackTitle && (p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle)) || (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) {
|
||||
group.push(j);
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the one with non-empty trackArtUrl, or fallback to the first
|
||||
let chosenIdx = group.find(idx => players[idx].trackArtUrl && players[idx].trackArtUrl.length > 0);
|
||||
if (chosenIdx === undefined)
|
||||
chosenIdx = group[0];
|
||||
|
||||
filtered.push(players[chosenIdx]);
|
||||
group.forEach(idx => used.add(idx));
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProc
|
||||
running: mediaControlsLoader.active
|
||||
onRunningChanged: {
|
||||
if (!cavaProc.running) {
|
||||
root.visualizerPoints = [];
|
||||
}
|
||||
}
|
||||
command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
// Parse `;`-separated values into the visualizerPoints array
|
||||
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
|
||||
root.visualizerPoints = points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: mediaControlsLoader
|
||||
active: GlobalStates.mediaControlsOpen
|
||||
onActiveChanged: {
|
||||
if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) {
|
||||
GlobalStates.mediaControlsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: mediaControlsRoot
|
||||
visible: true
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
exclusiveZone: 0
|
||||
implicitWidth: root.widgetWidth
|
||||
implicitHeight: playerColumnLayout.implicitHeight
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "quickshell:mediaControls"
|
||||
|
||||
anchors {
|
||||
top: !Config.options.bar.bottom || Config.options.bar.vertical
|
||||
bottom: Config.options.bar.bottom && !Config.options.bar.vertical
|
||||
left: !(Config.options.bar.vertical && Config.options.bar.bottom)
|
||||
right: Config.options.bar.vertical && Config.options.bar.bottom
|
||||
}
|
||||
margins {
|
||||
top: Config.options.bar.vertical ? ((mediaControlsRoot.screen.height / 2) - widgetHeight * 1.5) : Appearance.sizes.barHeight
|
||||
bottom: Appearance.sizes.barHeight
|
||||
left: Config.options.bar.vertical ? Appearance.sizes.barHeight : ((mediaControlsRoot.screen.width / 2) - (osdWidth / 2) - widgetWidth)
|
||||
right: Appearance.sizes.barHeight
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: playerColumnLayout
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [mediaControlsRoot]
|
||||
active: mediaControlsLoader.active
|
||||
onCleared: () => {
|
||||
if (!active) {
|
||||
GlobalStates.mediaControlsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: playerColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: -Appearance.sizes.elevationMargin // Shadow overlap okay
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.meaningfulPlayers
|
||||
}
|
||||
delegate: PlayerControl {
|
||||
required property MprisPlayer modelData
|
||||
player: modelData
|
||||
visualizerPoints: root.visualizerPoints
|
||||
implicitWidth: root.widgetWidth
|
||||
implicitHeight: root.widgetHeight
|
||||
radius: root.popupRounding
|
||||
}
|
||||
}
|
||||
|
||||
Item { // No player placeholder
|
||||
Layout.alignment: {
|
||||
if (mediaControlsRoot.anchors.left) return Qt.AlignLeft;
|
||||
if (mediaControlsRoot.anchors.right) return Qt.AlignRight;
|
||||
return Qt.AlignHCenter;
|
||||
}
|
||||
Layout.leftMargin: Appearance.sizes.hyprlandGapsOut
|
||||
Layout.rightMargin: Appearance.sizes.hyprlandGapsOut
|
||||
visible: root.meaningfulPlayers.length === 0
|
||||
implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin
|
||||
implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: placeholderBackground
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: placeholderBackground
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colLayer0
|
||||
radius: root.popupRounding
|
||||
property real padding: 20
|
||||
implicitWidth: placeholderLayout.implicitWidth + padding * 2
|
||||
implicitHeight: placeholderLayout.implicitHeight + padding * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: placeholderLayout
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
text: Translation.tr("No active player")
|
||||
font.pixelSize: Appearance.font.pixelSize.large
|
||||
}
|
||||
StyledText {
|
||||
color: Appearance.colors.colSubtext
|
||||
text: Translation.tr("Make sure your player has MPRIS support\nor try turning off duplicate player filtering")
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "mediaControls"
|
||||
|
||||
function toggle(): void {
|
||||
mediaControlsLoader.active = !mediaControlsLoader.active;
|
||||
if (mediaControlsLoader.active)
|
||||
Notifications.timeoutAll();
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
mediaControlsLoader.active = false;
|
||||
}
|
||||
|
||||
function open(): void {
|
||||
mediaControlsLoader.active = true;
|
||||
Notifications.timeoutAll();
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "mediaControlsToggle"
|
||||
description: "Toggles media controls on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "mediaControlsOpen"
|
||||
description: "Opens media controls on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.mediaControlsOpen = true;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "mediaControlsClose"
|
||||
description: "Closes media controls on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.mediaControlsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs.modules.common
|
||||
import qs.modules.common.models
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Item { // Player instance
|
||||
id: root
|
||||
required property MprisPlayer player
|
||||
property var artUrl: player?.trackArtUrl
|
||||
property string artDownloadLocation: Directories.coverArt
|
||||
property string artFileName: Qt.md5(artUrl)
|
||||
property string artFilePath: `${artDownloadLocation}/${artFileName}`
|
||||
property color artDominantColor: ColorUtils.mix((colorQuantizer?.colors[0] ?? Appearance.colors.colPrimary), Appearance.colors.colPrimaryContainer, 0.8) || Appearance.m3colors.m3secondaryContainer
|
||||
property bool downloaded: false
|
||||
property list<real> visualizerPoints: []
|
||||
property real maxVisualizerValue: 1000 // Max value in the data points
|
||||
property int visualizerSmoothing: 2 // Number of points to average for smoothing
|
||||
property real radius
|
||||
|
||||
property string displayedArtFilePath: root.downloaded ? Qt.resolvedUrl(artFilePath) : ""
|
||||
|
||||
component TrackChangeButton: RippleButton {
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
|
||||
property var iconName
|
||||
colBackground: ColorUtils.transparentize(blendedColors.colSecondaryContainer, 1)
|
||||
colBackgroundHover: blendedColors.colSecondaryContainerHover
|
||||
colRipple: blendedColors.colSecondaryContainerActive
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
fill: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: blendedColors.colOnSecondaryContainer
|
||||
text: iconName
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer { // Force update for revision
|
||||
running: root.player?.playbackState == MprisPlaybackState.Playing
|
||||
interval: Config.options.resources.updateInterval
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.player.positionChanged()
|
||||
}
|
||||
}
|
||||
|
||||
onArtFilePathChanged: {
|
||||
if (root.artUrl.length == 0) {
|
||||
root.artDominantColor = Appearance.m3colors.m3secondaryContainer
|
||||
return;
|
||||
}
|
||||
|
||||
// Binding does not work in Process
|
||||
coverArtDownloader.targetFile = root.artUrl
|
||||
coverArtDownloader.artFilePath = root.artFilePath
|
||||
// Download
|
||||
root.downloaded = false
|
||||
coverArtDownloader.running = true
|
||||
}
|
||||
|
||||
Process { // Cover art downloader
|
||||
id: coverArtDownloader
|
||||
property string targetFile: root.artUrl
|
||||
property string artFilePath: root.artFilePath
|
||||
command: [ "bash", "-c", `[ -f ${artFilePath} ] || curl -sSL '${targetFile}' -o '${artFilePath}'` ]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.downloaded = true
|
||||
}
|
||||
}
|
||||
|
||||
ColorQuantizer {
|
||||
id: colorQuantizer
|
||||
source: root.displayedArtFilePath
|
||||
depth: 0 // 2^0 = 1 color
|
||||
rescaleSize: 1 // Rescale to 1x1 pixel for faster processing
|
||||
}
|
||||
|
||||
property QtObject blendedColors: AdaptedMaterialScheme {
|
||||
color: artDominantColor
|
||||
}
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: background
|
||||
}
|
||||
Rectangle { // Background
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.sizes.elevationMargin
|
||||
color: ColorUtils.applyAlpha(blendedColors.colLayer0, 1)
|
||||
radius: root.radius
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: background.width
|
||||
height: background.height
|
||||
radius: background.radius
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: blurredArt
|
||||
anchors.fill: parent
|
||||
source: root.displayedArtFilePath
|
||||
sourceSize.width: background.width
|
||||
sourceSize.height: background.height
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: StyledBlurEffect {
|
||||
source: blurredArt
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3)
|
||||
radius: root.radius
|
||||
}
|
||||
}
|
||||
|
||||
WaveVisualizer {
|
||||
id: visualizerCanvas
|
||||
anchors.fill: parent
|
||||
live: root.player?.isPlaying
|
||||
points: root.visualizerPoints
|
||||
maxVisualizerValue: root.maxVisualizerValue
|
||||
smoothing: root.visualizerSmoothing
|
||||
color: blendedColors.colPrimary
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 13
|
||||
spacing: 15
|
||||
|
||||
Rectangle { // Art background
|
||||
id: artBackground
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: height
|
||||
radius: Appearance.rounding.verysmall
|
||||
color: ColorUtils.transparentize(blendedColors.colLayer1, 0.5)
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: artBackground.width
|
||||
height: artBackground.height
|
||||
radius: artBackground.radius
|
||||
}
|
||||
}
|
||||
|
||||
StyledImage { // Art image
|
||||
id: mediaArt
|
||||
property int size: parent.height
|
||||
anchors.fill: parent
|
||||
|
||||
source: root.displayedArtFilePath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
antialiasing: true
|
||||
|
||||
width: size
|
||||
height: size
|
||||
sourceSize.width: size
|
||||
sourceSize.height: size
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout { // Info & controls
|
||||
Layout.fillHeight: true
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
id: trackTitle
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Appearance.font.pixelSize.large
|
||||
color: blendedColors.colOnLayer0
|
||||
elide: Text.ElideRight
|
||||
text: StringUtils.cleanMusicTitle(root.player?.trackTitle) || "Untitled"
|
||||
animateChange: true
|
||||
animationDistanceX: 6
|
||||
animationDistanceY: 0
|
||||
}
|
||||
StyledText {
|
||||
id: trackArtist
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: blendedColors.colSubtext
|
||||
elide: Text.ElideRight
|
||||
text: root.player?.trackArtist
|
||||
animateChange: true
|
||||
animationDistanceX: 6
|
||||
animationDistanceY: 0
|
||||
}
|
||||
Item { Layout.fillHeight: true }
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: trackTime.implicitHeight + sliderRow.implicitHeight
|
||||
|
||||
StyledText {
|
||||
id: trackTime
|
||||
anchors.bottom: sliderRow.top
|
||||
anchors.bottomMargin: 5
|
||||
anchors.left: parent.left
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: blendedColors.colSubtext
|
||||
elide: Text.ElideRight
|
||||
text: `${StringUtils.friendlyTimeForSeconds(root.player?.position)} / ${StringUtils.friendlyTimeForSeconds(root.player?.length)}`
|
||||
}
|
||||
RowLayout {
|
||||
id: sliderRow
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
TrackChangeButton {
|
||||
iconName: "skip_previous"
|
||||
downAction: () => root.player?.previous()
|
||||
}
|
||||
Item {
|
||||
id: progressBarContainer
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.max(sliderLoader.implicitHeight, progressBarLoader.implicitHeight)
|
||||
|
||||
Loader {
|
||||
id: sliderLoader
|
||||
anchors.fill: parent
|
||||
active: root.player?.canSeek ?? false
|
||||
sourceComponent: StyledSlider {
|
||||
configuration: StyledSlider.Configuration.Wavy
|
||||
highlightColor: blendedColors.colPrimary
|
||||
trackColor: blendedColors.colSecondaryContainer
|
||||
handleColor: blendedColors.colPrimary
|
||||
value: root.player?.position / root.player?.length
|
||||
onMoved: {
|
||||
root.player.position = value * root.player.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: progressBarLoader
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
active: !(root.player?.canSeek ?? false)
|
||||
sourceComponent: StyledProgressBar {
|
||||
wavy: root.player?.isPlaying
|
||||
highlightColor: blendedColors.colPrimary
|
||||
trackColor: blendedColors.colSecondaryContainer
|
||||
value: root.player?.position / root.player?.length
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
TrackChangeButton {
|
||||
iconName: "skip_next"
|
||||
downAction: () => root.player?.next()
|
||||
}
|
||||
}
|
||||
|
||||
RippleButton {
|
||||
id: playPauseButton
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: sliderRow.top
|
||||
anchors.bottomMargin: 5
|
||||
property real size: 44
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
downAction: () => root.player.togglePlaying();
|
||||
|
||||
buttonRadius: root.player?.isPlaying ? Appearance?.rounding.normal : size / 2
|
||||
colBackground: root.player?.isPlaying ? blendedColors.colPrimary : blendedColors.colSecondaryContainer
|
||||
colBackgroundHover: root.player?.isPlaying ? blendedColors.colPrimaryHover : blendedColors.colSecondaryContainerHover
|
||||
colRipple: root.player?.isPlaying ? blendedColors.colPrimaryActive : blendedColors.colSecondaryContainerActive
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
fill: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: root.player?.isPlaying ? blendedColors.colOnPrimary : blendedColors.colOnSecondaryContainer
|
||||
text: root.player?.isPlaying ? "pause" : "play_arrow"
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: notificationPopup
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
visible: (Notifications.popupList.length > 0) && !GlobalStates.screenLocked
|
||||
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? null
|
||||
|
||||
WlrLayershell.namespace: "quickshell:notificationPopup"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
exclusiveZone: 0
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: listview.contentItem
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
implicitWidth: Appearance.sizes.notificationPopupWidth
|
||||
|
||||
NotificationListView {
|
||||
id: listview
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
rightMargin: 4
|
||||
topMargin: 4
|
||||
}
|
||||
implicitWidth: parent.width - Appearance.sizes.elevationMargin * 2
|
||||
popup: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
property string protectionMessage: ""
|
||||
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
|
||||
|
||||
property string currentIndicator: "volume"
|
||||
property var indicators: [
|
||||
{
|
||||
id: "volume",
|
||||
sourceUrl: "indicators/VolumeIndicator.qml"
|
||||
},
|
||||
{
|
||||
id: "brightness",
|
||||
sourceUrl: "indicators/BrightnessIndicator.qml"
|
||||
},
|
||||
]
|
||||
|
||||
function triggerOsd() {
|
||||
GlobalStates.osdVolumeOpen = true;
|
||||
osdTimeout.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: osdTimeout
|
||||
interval: Config.options.osd.timeout
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
GlobalStates.osdVolumeOpen = false;
|
||||
root.protectionMessage = "";
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Brightness
|
||||
function onBrightnessChanged() {
|
||||
root.protectionMessage = "";
|
||||
root.currentIndicator = "brightness";
|
||||
root.triggerOsd();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
// Listen to volume changes
|
||||
target: Audio.sink?.audio ?? null
|
||||
function onVolumeChanged() {
|
||||
if (!Audio.ready)
|
||||
return;
|
||||
root.currentIndicator = "volume";
|
||||
root.triggerOsd();
|
||||
}
|
||||
function onMutedChanged() {
|
||||
if (!Audio.ready)
|
||||
return;
|
||||
root.currentIndicator = "volume";
|
||||
root.triggerOsd();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
// Listen to protection triggers
|
||||
target: Audio
|
||||
function onSinkProtectionTriggered(reason) {
|
||||
root.protectionMessage = reason;
|
||||
root.currentIndicator = "volume";
|
||||
root.triggerOsd();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: osdLoader
|
||||
active: GlobalStates.osdVolumeOpen
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: osdRoot
|
||||
color: "transparent"
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onFocusedScreenChanged() {
|
||||
osdRoot.screen = root.focusedScreen;
|
||||
}
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "quickshell:onScreenDisplay"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
anchors {
|
||||
top: !Config.options.bar.bottom
|
||||
bottom: Config.options.bar.bottom
|
||||
}
|
||||
mask: Region {
|
||||
item: osdValuesWrapper
|
||||
}
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
exclusiveZone: 0
|
||||
margins {
|
||||
top: Appearance.sizes.barHeight
|
||||
bottom: Appearance.sizes.barHeight
|
||||
}
|
||||
|
||||
implicitWidth: columnLayout.implicitWidth
|
||||
implicitHeight: columnLayout.implicitHeight
|
||||
visible: osdLoader.active
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Item {
|
||||
id: osdValuesWrapper
|
||||
// Extra space for shadow
|
||||
implicitHeight: contentColumnLayout.implicitHeight
|
||||
implicitWidth: contentColumnLayout.implicitWidth
|
||||
clip: true
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: GlobalStates.osdVolumeOpen = false
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumnLayout
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
id: osdIndicatorLoader
|
||||
source: root.indicators.find(i => i.id === root.currentIndicator)?.sourceUrl
|
||||
}
|
||||
|
||||
Item {
|
||||
id: protectionMessageWrapper
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitHeight: protectionMessageBackground.implicitHeight
|
||||
implicitWidth: protectionMessageBackground.implicitWidth
|
||||
opacity: root.protectionMessage !== "" ? 1 : 0
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: protectionMessageBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: protectionMessageBackground
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.m3colors.m3error
|
||||
property real padding: 10
|
||||
implicitHeight: protectionMessageRowLayout.implicitHeight + padding * 2
|
||||
implicitWidth: protectionMessageRowLayout.implicitWidth + padding * 2
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
RowLayout {
|
||||
id: protectionMessageRowLayout
|
||||
anchors.centerIn: parent
|
||||
MaterialSymbol {
|
||||
id: protectionMessageIcon
|
||||
text: "dangerous"
|
||||
iconSize: Appearance.font.pixelSize.hugeass
|
||||
color: Appearance.m3colors.m3onError
|
||||
}
|
||||
StyledText {
|
||||
id: protectionMessageTextWidget
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Appearance.m3colors.m3onError
|
||||
wrapMode: Text.Wrap
|
||||
text: root.protectionMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "osdVolume"
|
||||
|
||||
function trigger() {
|
||||
root.triggerOsd();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
GlobalStates.osdVolumeOpen = false;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
GlobalStates.osdVolumeOpen = !GlobalStates.osdVolumeOpen;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "osdVolumeTrigger"
|
||||
description: "Triggers volume OSD on press"
|
||||
|
||||
onPressed: {
|
||||
root.triggerOsd();
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "osdVolumeHide"
|
||||
description: "Hides volume OSD on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.osdVolumeOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property real value
|
||||
required property string icon
|
||||
required property string name
|
||||
property bool rotateIcon: false
|
||||
property bool scaleIcon: false
|
||||
|
||||
property real valueIndicatorVerticalPadding: 9
|
||||
property real valueIndicatorLeftPadding: 10
|
||||
property real valueIndicatorRightPadding: 20 // An icon is circle ish, a column isn't, hence the extra padding
|
||||
|
||||
implicitWidth: Appearance.sizes.osdWidth + 2 * Appearance.sizes.elevationMargin
|
||||
implicitHeight: valueIndicator.implicitHeight + 2 * Appearance.sizes.elevationMargin
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: valueIndicator
|
||||
}
|
||||
Rectangle {
|
||||
id: valueIndicator
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Appearance.sizes.elevationMargin
|
||||
}
|
||||
radius: Appearance.rounding.full
|
||||
color: Appearance.colors.colLayer0
|
||||
|
||||
implicitWidth: valueRow.implicitWidth
|
||||
implicitHeight: valueRow.implicitHeight
|
||||
|
||||
RowLayout { // Icon on the left, stuff on the right
|
||||
id: valueRow
|
||||
Layout.margins: 10
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
Item {
|
||||
implicitWidth: 30
|
||||
implicitHeight: 30
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: valueIndicatorLeftPadding
|
||||
Layout.topMargin: valueIndicatorVerticalPadding
|
||||
Layout.bottomMargin: valueIndicatorVerticalPadding
|
||||
|
||||
MaterialSymbol { // Icon
|
||||
anchors {
|
||||
centerIn: parent
|
||||
alignWhenCentered: !root.rotateIcon
|
||||
}
|
||||
color: Appearance.colors.colOnLayer0
|
||||
renderType: Text.QtRendering
|
||||
|
||||
text: root.icon
|
||||
iconSize: 20 + 10 * (root.scaleIcon ? value : 1)
|
||||
rotation: 180 * (root.rotateIcon ? value : 0)
|
||||
|
||||
Behavior on iconSize {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on rotation {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
ColumnLayout { // Stuff
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: valueIndicatorRightPadding
|
||||
spacing: 5
|
||||
|
||||
RowLayout { // Name fill left, value on the right end
|
||||
Layout.leftMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end
|
||||
Layout.rightMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end
|
||||
|
||||
StyledText {
|
||||
color: Appearance.colors.colOnLayer0
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
Layout.fillWidth: true
|
||||
text: root.name
|
||||
}
|
||||
|
||||
StyledText {
|
||||
color: Appearance.colors.colOnLayer0
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
Layout.fillWidth: false
|
||||
text: Math.round(root.value * 100)
|
||||
}
|
||||
}
|
||||
|
||||
StyledProgressBar {
|
||||
id: valueProgressBar
|
||||
Layout.fillWidth: true
|
||||
value: root.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import qs.modules.ii.onScreenDisplay
|
||||
|
||||
OsdValueIndicator {
|
||||
id: root
|
||||
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
|
||||
property var brightnessMonitor: Brightness.getMonitorForScreen(focusedScreen)
|
||||
|
||||
icon: Hyprsunset.active ? "routine" : "light_mode"
|
||||
rotateIcon: true
|
||||
scaleIcon: true
|
||||
name: Translation.tr("Brightness")
|
||||
value: root.brightnessMonitor?.brightness ?? 50
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import qs.modules.ii.onScreenDisplay
|
||||
|
||||
OsdValueIndicator {
|
||||
id: osdValues
|
||||
value: Audio.sink?.audio.volume ?? 0
|
||||
icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up"
|
||||
name: Translation.tr("Volume")
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope { // Scope
|
||||
id: root
|
||||
property bool pinned: Config.options?.osk.pinnedOnStartup ?? false
|
||||
|
||||
component OskControlButton: GroupButton { // Pin button
|
||||
baseWidth: 40
|
||||
baseHeight: 40
|
||||
clickedWidth: baseWidth
|
||||
clickedHeight: baseHeight + 10
|
||||
buttonRadius: Appearance.rounding.normal
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: oskLoader
|
||||
active: GlobalStates.oskOpen
|
||||
onActiveChanged: {
|
||||
if (!oskLoader.active) {
|
||||
Ydotool.releaseAllKeys();
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow { // Window
|
||||
id: oskRoot
|
||||
visible: oskLoader.active && !GlobalStates.screenLocked
|
||||
|
||||
anchors {
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
GlobalStates.oskOpen = false
|
||||
}
|
||||
exclusiveZone: root.pinned ? implicitHeight - Appearance.sizes.hyprlandGapsOut : 0
|
||||
implicitWidth: oskBackground.width + Appearance.sizes.elevationMargin * 2
|
||||
implicitHeight: oskBackground.height + Appearance.sizes.elevationMargin * 2
|
||||
WlrLayershell.namespace: "quickshell:osk"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
// Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
|
||||
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {
|
||||
item: oskBackground
|
||||
}
|
||||
|
||||
|
||||
// Background
|
||||
StyledRectangularShadow {
|
||||
target: oskBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: oskBackground
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colLayer0
|
||||
radius: Appearance.rounding.windowRounding
|
||||
property real padding: 10
|
||||
implicitWidth: oskRowLayout.implicitWidth + padding * 2
|
||||
implicitHeight: oskRowLayout.implicitHeight + padding * 2
|
||||
|
||||
Keys.onPressed: (event) => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
oskRoot.hide()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: oskRowLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
VerticalButtonGroup {
|
||||
OskControlButton { // Pin button
|
||||
toggled: root.pinned
|
||||
downAction: () => root.pinned = !root.pinned
|
||||
contentItem: MaterialSymbol {
|
||||
text: "keep"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
|
||||
}
|
||||
}
|
||||
OskControlButton {
|
||||
onClicked: () => {
|
||||
oskRoot.hide()
|
||||
}
|
||||
contentItem: MaterialSymbol {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "keyboard_hide"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 20
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 1
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
}
|
||||
OskContent {
|
||||
id: oskContent
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "osk"
|
||||
|
||||
function toggle(): void {
|
||||
GlobalStates.oskOpen = !GlobalStates.oskOpen;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
GlobalStates.oskOpen = false
|
||||
}
|
||||
|
||||
function open(): void {
|
||||
GlobalStates.oskOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "oskToggle"
|
||||
description: "Toggles on screen keyboard on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.oskOpen = !GlobalStates.oskOpen;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "oskOpen"
|
||||
description: "Opens on screen keyboard on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.oskOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "oskClose"
|
||||
description: "Closes on screen keyboard on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.oskOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import qs.modules.common
|
||||
import "layouts.js" as Layouts
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var layouts: Layouts.byName
|
||||
property var activeLayoutName: (layouts.hasOwnProperty(Config.options?.osk.layout))
|
||||
? Config.options?.osk.layout
|
||||
: Layouts.defaultLayout
|
||||
property var currentLayout: layouts[activeLayoutName]
|
||||
|
||||
implicitWidth: keyRows.implicitWidth
|
||||
implicitHeight: keyRows.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: keyRows
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
model: root.currentLayout.keys
|
||||
|
||||
delegate: RowLayout {
|
||||
id: keyRow
|
||||
required property var modelData
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
model: modelData
|
||||
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
|
||||
delegate: OskKey {
|
||||
required property var modelData
|
||||
keyData: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property var keyData
|
||||
property string key: keyData.label
|
||||
property string type: keyData.keytype
|
||||
property var keycode: keyData.keycode
|
||||
property string shape: keyData.shape
|
||||
property bool isShift: Ydotool.shiftKeys.includes(keycode)
|
||||
property bool isBackspace: (key.toLowerCase() == "backspace")
|
||||
property bool isEnter: (key.toLowerCase() == "enter" || key.toLowerCase() == "return")
|
||||
property real baseWidth: 45
|
||||
property real baseHeight: 45
|
||||
property var widthMultiplier: ({
|
||||
"normal": 1,
|
||||
"fn": 1,
|
||||
"tab": 1.6,
|
||||
"caps": 1.9,
|
||||
"shift": 2.5,
|
||||
"control": 1.3
|
||||
})
|
||||
property var heightMultiplier: ({
|
||||
"normal": 1,
|
||||
"fn": 0.7,
|
||||
"tab": 1,
|
||||
"caps": 1,
|
||||
"shift": 1,
|
||||
"control": 1
|
||||
})
|
||||
toggled: isShift ? Ydotool.shiftMode : false
|
||||
|
||||
enabled: shape != "empty"
|
||||
colBackground: shape == "empty" ? ColorUtils.transparentize(Appearance.colors.colLayer1) : Appearance.colors.colLayer1
|
||||
buttonRadius: Appearance.rounding.small
|
||||
implicitWidth: baseWidth * widthMultiplier[shape] || baseWidth
|
||||
implicitHeight: baseHeight * heightMultiplier[shape] || baseHeight
|
||||
Layout.fillWidth: shape == "space" || shape == "expand"
|
||||
|
||||
Connections {
|
||||
target: Ydotool
|
||||
enabled: isShift
|
||||
function onShiftModeChanged() {
|
||||
if (Ydotool.shiftMode == 0) {
|
||||
capsLockTimer.hasStarted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: capsLockTimer
|
||||
property bool hasStarted: false
|
||||
property bool canCaps: false
|
||||
interval: 300
|
||||
function startWaiting() {
|
||||
hasStarted = true;
|
||||
canCaps = true;
|
||||
start();
|
||||
}
|
||||
onTriggered: {
|
||||
canCaps = false;
|
||||
}
|
||||
}
|
||||
|
||||
downAction: () => {
|
||||
Ydotool.press(root.keycode);
|
||||
if (isShift && Ydotool.shiftMode == 0) Ydotool.shiftMode = 1;
|
||||
}
|
||||
releaseAction: () => {
|
||||
if (root.type == "normal") {
|
||||
Ydotool.release(root.keycode);
|
||||
if (Ydotool.shiftMode == 1) {
|
||||
Ydotool.releaseShiftKeys()
|
||||
}
|
||||
} else if (isShift) {
|
||||
if (Ydotool.shiftMode == 1) {
|
||||
if (!capsLockTimer.hasStarted) {
|
||||
capsLockTimer.startWaiting();
|
||||
} else {
|
||||
if (capsLockTimer.canCaps) {
|
||||
Ydotool.shiftMode = 2; // Caps lock mode
|
||||
} else {
|
||||
Ydotool.releaseShiftKeys()
|
||||
}
|
||||
}
|
||||
} else if (Ydotool.shiftMode == 2) {
|
||||
Ydotool.releaseShiftKeys();
|
||||
}
|
||||
} else if (root.type == "modkey") {
|
||||
root.toggled = !root.toggled;
|
||||
if (!root.toggled) {
|
||||
if (isShift) {
|
||||
Ydotool.releaseShiftKeys();
|
||||
} else {
|
||||
Ydotool.release(root.keycode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
contentItem: StyledText {
|
||||
id: keyText
|
||||
anchors.fill: parent
|
||||
font.family: (isBackspace || isEnter) ? Appearance.font.family.iconMaterial : Appearance.font.family.main
|
||||
font.pixelSize: root.shape == "fn" ? Appearance.font.pixelSize.small :
|
||||
(isBackspace || isEnter) ? Appearance.font.pixelSize.huge :
|
||||
Appearance.font.pixelSize.large
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: root.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
|
||||
text: root.isBackspace ? "backspace" : root.isEnter ? "subdirectory_arrow_left" :
|
||||
Ydotool.shiftMode == 2 ? (root.keyData.labelCaps || root.keyData.labelShift || root.keyData.label) :
|
||||
Ydotool.shiftMode == 1 ? (root.keyData.labelShift || root.keyData.label) :
|
||||
root.keyData.label
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// We're going to use ydotool
|
||||
// See /usr/include/linux/input-event-codes.h for keycodes
|
||||
|
||||
const defaultLayout = "English (US)";
|
||||
const byName = {
|
||||
"English (US)": {
|
||||
name_short: "US",
|
||||
description: "QWERTY - Full",
|
||||
comment: "Like physical keyboard",
|
||||
// A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type)
|
||||
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
|
||||
// keys: [
|
||||
// [{ k: "Esc", t: "fn" }, { k: "F1", t: "fn" }, { k: "F2", t: "fn" }, { k: "F3", t: "fn" }, { k: "F4", t: "fn" }, { k: "F5", t: "fn" }, { k: "F6", t: "fn" }, { k: "F7", t: "fn" }, { k: "F8", t: "fn" }, { k: "F9", t: "fn" }, { k: "F10", t: "fn" }, { k: "F11", t: "fn" }, { k: "F12", t: "fn" }, { k: "PrtSc", t: "fn" }, { k: "Del", t: "fn" }],
|
||||
// [{ k: "`", ks: "~", t: "normal" }, { k: "1", ks: "!", t: "normal" }, { k: "2", ks: "@", t: "normal" }, { k: "3", ks: "#", t: "normal" }, { k: "4", ks: "$", t: "normal" }, { k: "5", ks: "%", t: "normal" }, { k: "6", ks: "^", t: "normal" }, { k: "7", ks: "&", t: "normal" }, { k: "8", ks: "*", t: "normal" }, { k: "9", ks: "(", t: "normal" }, { k: "0", ks: ")", t: "normal" }, { k: "-", ks: "_", t: "normal" }, { k: "=", ks: "+", t: "normal" }, { k: "Backspace", t: "shift" }],
|
||||
// [{ k: "Tab", t: "tab" }, { k: "q", ks: "Q", t: "normal" }, { k: "w", ks: "W", t: "normal" }, { k: "e", ks: "E", t: "normal" }, { k: "r", ks: "R", t: "normal" }, { k: "t", ks: "T", t: "normal" }, { k: "y", ks: "Y", t: "normal" }, { k: "u", ks: "U", t: "normal" }, { k: "i", ks: "I", t: "normal" }, { k: "o", ks: "O", t: "normal" }, { k: "p", ks: "P", t: "normal" }, { k: "[", ks: "{", t: "normal" }, { k: "]", ks: "}", t: "normal" }, { k: "\\", ks: "|", t: "expand" }],
|
||||
// [{ k: "Caps", t: "caps" }, { k: "a", ks: "A", t: "normal" }, { k: "s", ks: "S", t: "normal" }, { k: "d", ks: "D", t: "normal" }, { k: "f", ks: "F", t: "normal" }, { k: "g", ks: "G", t: "normal" }, { k: "h", ks: "H", t: "normal" }, { k: "j", ks: "J", t: "normal" }, { k: "k", ks: "K", t: "normal" }, { k: "l", ks: "L", t: "normal" }, { k: ";", ks: ":", t: "normal" }, { k: "'", ks: '"', t: "normal" }, { k: "Enter", t: "expand" }],
|
||||
// [{ k: "Shift", t: "shift" }, { k: "z", ks: "Z", t: "normal" }, { k: "x", ks: "X", t: "normal" }, { k: "c", ks: "C", t: "normal" }, { k: "v", ks: "V", t: "normal" }, { k: "b", ks: "B", t: "normal" }, { k: "n", ks: "N", t: "normal" }, { k: "m", ks: "M", t: "normal" }, { k: ",", ks: "<", t: "normal" }, { k: ".", ks: ">", t: "normal" }, { k: "/", ks: "?", t: "normal" }, { k: "Shift", t: "expand" }],
|
||||
// [{ k: "Ctrl", t: "control" }, { k: "Fn", t: "normal" }, { k: "Win", t: "normal" }, { k: "Alt", t: "normal" }, { k: "Space", t: "space" }, { k: "Alt", t: "normal" }, { k: "Menu", t: "normal" }, { k: "Ctrl", t: "control" }]
|
||||
// ]
|
||||
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
|
||||
// A modkey looks like this: {label: "Ctrl", shape: "control", keycode: 29, type: "modkey"}
|
||||
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
|
||||
keys: [
|
||||
[
|
||||
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
|
||||
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
|
||||
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
|
||||
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
|
||||
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
|
||||
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
|
||||
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
|
||||
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
|
||||
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
|
||||
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
|
||||
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
|
||||
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
|
||||
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
|
||||
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
|
||||
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "`", labelShift: "~", shape: "normal", keycode: 41 },
|
||||
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
|
||||
{ keytype: "normal", label: "2", labelShift: "@", shape: "normal", keycode: 3 },
|
||||
{ keytype: "normal", label: "3", labelShift: "#", shape: "normal", keycode: 4 },
|
||||
{ keytype: "normal", label: "4", labelShift: "$", shape: "normal", keycode: 5 },
|
||||
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
|
||||
{ keytype: "normal", label: "6", labelShift: "^", shape: "normal", keycode: 7 },
|
||||
{ keytype: "normal", label: "7", labelShift: "&", shape: "normal", keycode: 8 },
|
||||
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
|
||||
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
|
||||
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
|
||||
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
|
||||
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
|
||||
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
|
||||
{ keytype: "normal", label: "q", labelShift: "Q", shape: "normal", keycode: 16 },
|
||||
{ keytype: "normal", label: "w", labelShift: "W", shape: "normal", keycode: 17 },
|
||||
{ keytype: "normal", label: "e", labelShift: "E", shape: "normal", keycode: 18 },
|
||||
{ keytype: "normal", label: "r", labelShift: "R", shape: "normal", keycode: 19 },
|
||||
{ keytype: "normal", label: "t", labelShift: "T", shape: "normal", keycode: 20 },
|
||||
{ keytype: "normal", label: "y", labelShift: "Y", shape: "normal", keycode: 21 },
|
||||
{ keytype: "normal", label: "u", labelShift: "U", shape: "normal", keycode: 22 },
|
||||
{ keytype: "normal", label: "i", labelShift: "I", shape: "normal", keycode: 23 },
|
||||
{ keytype: "normal", label: "o", labelShift: "O", shape: "normal", keycode: 24 },
|
||||
{ keytype: "normal", label: "p", labelShift: "P", shape: "normal", keycode: 25 },
|
||||
{ keytype: "normal", label: "[", labelShift: "{", shape: "normal", keycode: 26 },
|
||||
{ keytype: "normal", label: "]", labelShift: "}", shape: "normal", keycode: 27 },
|
||||
{ keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
|
||||
],
|
||||
[
|
||||
//{ keytype: "normal", label: "Caps", shape: "caps", keycode: 58 }, // not needed as double-pressing shift does that
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "normal", label: "a", labelShift: "A", shape: "normal", keycode: 30 },
|
||||
{ keytype: "normal", label: "s", labelShift: "S", shape: "normal", keycode: 31 },
|
||||
{ keytype: "normal", label: "d", labelShift: "D", shape: "normal", keycode: 32 },
|
||||
{ keytype: "normal", label: "f", labelShift: "F", shape: "normal", keycode: 33 },
|
||||
{ keytype: "normal", label: "g", labelShift: "G", shape: "normal", keycode: 34 },
|
||||
{ keytype: "normal", label: "h", labelShift: "H", shape: "normal", keycode: 35 },
|
||||
{ keytype: "normal", label: "j", labelShift: "J", shape: "normal", keycode: 36 },
|
||||
{ keytype: "normal", label: "k", labelShift: "K", shape: "normal", keycode: 37 },
|
||||
{ keytype: "normal", label: "l", labelShift: "L", shape: "normal", keycode: 38 },
|
||||
{ keytype: "normal", label: ";", labelShift: ":", shape: "normal", keycode: 39 },
|
||||
{ keytype: "normal", label: "'", labelShift: '"', shape: "normal", keycode: 40 },
|
||||
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "shift", keycode: 42 },
|
||||
{ keytype: "normal", label: "z", labelShift: "Z", shape: "normal", keycode: 44 },
|
||||
{ keytype: "normal", label: "x", labelShift: "X", shape: "normal", keycode: 45 },
|
||||
{ keytype: "normal", label: "c", labelShift: "C", shape: "normal", keycode: 46 },
|
||||
{ keytype: "normal", label: "v", labelShift: "V", shape: "normal", keycode: 47 },
|
||||
{ keytype: "normal", label: "b", labelShift: "B", shape: "normal", keycode: 48 },
|
||||
{ keytype: "normal", label: "n", labelShift: "N", shape: "normal", keycode: 49 },
|
||||
{ keytype: "normal", label: "m", labelShift: "M", shape: "normal", keycode: 50 },
|
||||
{ keytype: "normal", label: ",", labelShift: "<", shape: "normal", keycode: 51 },
|
||||
{ keytype: "normal", label: ".", labelShift: ">", shape: "normal", keycode: 52 },
|
||||
{ keytype: "normal", label: "/", labelShift: "?", shape: "normal", keycode: 53 },
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "expand", keycode: 54 } // optional
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
|
||||
// { label: "Super", shape: "normal", keycode: 125 }, // dangerous
|
||||
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
|
||||
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
|
||||
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
|
||||
// { label: "Super", shape: "normal", keycode: 126 }, // dangerous
|
||||
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
|
||||
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"German": {
|
||||
name_short: "DE",
|
||||
description: "QWERTZ - Full",
|
||||
comment: "Keyboard layout commonly used in German-speaking countries",
|
||||
keys: [
|
||||
[
|
||||
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
|
||||
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
|
||||
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
|
||||
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
|
||||
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
|
||||
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
|
||||
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
|
||||
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
|
||||
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
|
||||
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
|
||||
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
|
||||
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
|
||||
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
|
||||
{ keytype: "normal", label: "Druck", shape: "fn", keycode: 99 },
|
||||
{ keytype: "normal", label: "Entf", shape: "fn", keycode: 111 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "^", labelShift: "°", labelAlt: "′", shape: "normal", keycode: 41 },
|
||||
{ keytype: "normal", label: "1", labelShift: "!", labelAlt: "¹", shape: "normal", keycode: 2 },
|
||||
{ keytype: "normal", label: "2", labelShift: "\"", labelAlt: "²", shape: "normal", keycode: 3 },
|
||||
{ keytype: "normal", label: "3", labelShift: "§", labelAlt: "³", shape: "normal", keycode: 4 },
|
||||
{ keytype: "normal", label: "4", labelShift: "$", labelAlt: "¼", shape: "normal", keycode: 5 },
|
||||
{ keytype: "normal", label: "5", labelShift: "%", labelAlt: "½", shape: "normal", keycode: 6 },
|
||||
{ keytype: "normal", label: "6", labelShift: "&", labelAlt: "¬", shape: "normal", keycode: 7 },
|
||||
{ keytype: "normal", label: "7", labelShift: "/", labelAlt: "{", shape: "normal", keycode: 8 },
|
||||
{ keytype: "normal", label: "8", labelShift: "(", labelAlt: "[", shape: "normal", keycode: 9 },
|
||||
{ keytype: "normal", label: "9", labelShift: ")", labelAlt: "]", shape: "normal", keycode: 10 },
|
||||
{ keytype: "normal", label: "0", labelShift: "=", labelAlt: "}", shape: "normal", keycode: 11 },
|
||||
{ keytype: "normal", label: "ß", labelShift: "?", labelAlt: "\\", shape: "normal", keycode: 12 },
|
||||
{ keytype: "normal", label: "´", labelShift: "`", labelAlt: "¸", shape: "normal", keycode: 13 },
|
||||
{ keytype: "normal", label: "⟵", shape: "expand", keycode: 14 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "Tab ⇆", shape: "tab", keycode: 15 },
|
||||
{ keytype: "normal", label: "q", labelShift: "Q", labelAlt: "@", shape: "normal", keycode: 16 },
|
||||
{ keytype: "normal", label: "w", labelShift: "W", labelAlt: "ſ", shape: "normal", keycode: 17 },
|
||||
{ keytype: "normal", label: "e", labelShift: "E", labelAlt: "€", shape: "normal", keycode: 18 },
|
||||
{ keytype: "normal", label: "r", labelShift: "R", labelAlt: "¶", shape: "normal", keycode: 19 },
|
||||
{ keytype: "normal", label: "t", labelShift: "T", labelAlt: "ŧ", shape: "normal", keycode: 20 },
|
||||
{ keytype: "normal", label: "z", labelShift: "Z", labelAlt: "←", shape: "normal", keycode: 21 },
|
||||
{ keytype: "normal", label: "u", labelShift: "U", labelAlt: "↓", shape: "normal", keycode: 22 },
|
||||
{ keytype: "normal", label: "i", labelShift: "I", labelAlt: "→", shape: "normal", keycode: 23 },
|
||||
{ keytype: "normal", label: "o", labelShift: "O", labelAlt: "ø", shape: "normal", keycode: 24 },
|
||||
{ keytype: "normal", label: "p", labelShift: "P", labelAlt: "þ", shape: "normal", keycode: 25 },
|
||||
{ keytype: "normal", label: "ü", labelShift: "Ü", labelAlt: "¨", shape: "normal", keycode: 26 },
|
||||
{ keytype: "normal", label: "+", labelShift: "*", labelAlt: "~", shape: "normal", keycode: 27 },
|
||||
{ keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
//{ keytype: "normal", label: "Umschalt ⇩", shape: "caps", keycode: 58 },
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "normal", label: "a", labelShift: "A", labelAlt: "æ", shape: "normal", keycode: 30 },
|
||||
{ keytype: "normal", label: "s", labelShift: "S", labelAlt: "ſ", shape: "normal", keycode: 31 },
|
||||
{ keytype: "normal", label: "d", labelShift: "D", labelAlt: "ð", shape: "normal", keycode: 32 },
|
||||
{ keytype: "normal", label: "f", labelShift: "F", labelAlt: "đ", shape: "normal", keycode: 33 },
|
||||
{ keytype: "normal", label: "g", labelShift: "G", labelAlt: "ŋ", shape: "normal", keycode: 34 },
|
||||
{ keytype: "normal", label: "h", labelShift: "H", labelAlt: "ħ", shape: "normal", keycode: 35 },
|
||||
{ keytype: "normal", label: "j", labelShift: "J", labelAlt: "", shape: "normal", keycode: 36 },
|
||||
{ keytype: "normal", label: "k", labelShift: "K", labelAlt: "ĸ", shape: "normal", keycode: 37 },
|
||||
{ keytype: "normal", label: "l", labelShift: "L", labelAlt: "ł", shape: "normal", keycode: 38 },
|
||||
{ keytype: "normal", label: "ö", labelShift: "Ö", labelAlt: "˝", shape: "normal", keycode: 39 },
|
||||
{ keytype: "normal", label: "ä", labelShift: 'Ä', labelAlt: "^", shape: "normal", keycode: 40 },
|
||||
{ keytype: "normal", label: "#", labelShift: '\'', labelAlt: "’", shape: "normal", keycode: 43 },
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
//{ keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "shift", keycode: 42 },
|
||||
{ keytype: "normal", label: "<", labelShift: ">", labelAlt: "|", shape: "normal", keycode: 86 },
|
||||
{ keytype: "normal", label: "y", labelShift: "Y", labelAlt: "»", shape: "normal", keycode: 44 },
|
||||
{ keytype: "normal", label: "x", labelShift: "X", labelAlt: "«", shape: "normal", keycode: 45 },
|
||||
{ keytype: "normal", label: "c", labelShift: "C", labelAlt: "¢", shape: "normal", keycode: 46 },
|
||||
{ keytype: "normal", label: "v", labelShift: "V", labelAlt: "„", shape: "normal", keycode: 47 },
|
||||
{ keytype: "normal", label: "b", labelShift: "B", labelAlt: "“", shape: "normal", keycode: 48 },
|
||||
{ keytype: "normal", label: "n", labelShift: "N", labelAlt: "”", shape: "normal", keycode: 49 },
|
||||
{ keytype: "normal", label: "m", labelShift: "M", labelAlt: "µ", shape: "normal", keycode: 50 },
|
||||
{ keytype: "normal", label: ",", labelShift: ";", labelAlt: "·", shape: "normal", keycode: 51 },
|
||||
{ keytype: "normal", label: ".", labelShift: ":", labelAlt: "…", shape: "normal", keycode: 52 },
|
||||
{ keytype: "normal", label: "-", labelShift: "_", labelAlt: "–", shape: "normal", keycode: 53 },
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "expand", keycode: 54 }, // optional
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Strg", shape: "control", keycode: 29 },
|
||||
//{ keytype: "normal", label: "", shape: "normal", keycode: 125 }, // dangerous
|
||||
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
|
||||
{ keytype: "normal", label: "Leertaste", shape: "space", keycode: 57 },
|
||||
{ keytype: "modkey", label: "Alt Gr", shape: "normal", keycode: 100 },
|
||||
// { label: "Super", shape: "normal", keycode: 126 }, // dangerous
|
||||
//{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 }, // doesn't work?
|
||||
{ keytype: "modkey", label: "Strg", shape: "control", keycode: 97 },
|
||||
{ keytype: "normal", label: "⇦", shape: "normal", keycode: 105 },
|
||||
{ keytype: "normal", label: "⇨", shape: "normal", keycode: 106 },
|
||||
]
|
||||
]
|
||||
},
|
||||
"Russian": {
|
||||
name_short: "RU",
|
||||
description: "ЙЦУКЕН - Full",
|
||||
comment: "Standard Russian keyboard layout",
|
||||
keys: [
|
||||
[
|
||||
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
|
||||
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
|
||||
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
|
||||
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
|
||||
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
|
||||
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
|
||||
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
|
||||
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
|
||||
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
|
||||
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
|
||||
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
|
||||
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
|
||||
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
|
||||
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
|
||||
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "ё", labelShift: "Ё", shape: "normal", keycode: 41 },
|
||||
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
|
||||
{ keytype: "normal", label: "2", labelShift: "\"", shape: "normal", keycode: 3 },
|
||||
{ keytype: "normal", label: "3", labelShift: "№", shape: "normal", keycode: 4 },
|
||||
{ keytype: "normal", label: "4", labelShift: ";", shape: "normal", keycode: 5 },
|
||||
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
|
||||
{ keytype: "normal", label: "6", labelShift: ":", shape: "normal", keycode: 7 },
|
||||
{ keytype: "normal", label: "7", labelShift: "?", shape: "normal", keycode: 8 },
|
||||
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
|
||||
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
|
||||
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
|
||||
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
|
||||
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
|
||||
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
|
||||
],
|
||||
[
|
||||
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
|
||||
{ keytype: "normal", label: "й", labelShift: "Й", shape: "normal", keycode: 16 },
|
||||
{ keytype: "normal", label: "ц", labelShift: "Ц", shape: "normal", keycode: 17 },
|
||||
{ keytype: "normal", label: "у", labelShift: "У", shape: "normal", keycode: 18 },
|
||||
{ keytype: "normal", label: "к", labelShift: "К", shape: "normal", keycode: 19 },
|
||||
{ keytype: "normal", label: "е", labelShift: "Е", shape: "normal", keycode: 20 },
|
||||
{ keytype: "normal", label: "н", labelShift: "Н", shape: "normal", keycode: 21 },
|
||||
{ keytype: "normal", label: "г", labelShift: "Г", shape: "normal", keycode: 22 },
|
||||
{ keytype: "normal", label: "ш", labelShift: "Ш", shape: "normal", keycode: 23 },
|
||||
{ keytype: "normal", label: "щ", labelShift: "Щ", shape: "normal", keycode: 24 },
|
||||
{ keytype: "normal", label: "з", labelShift: "З", shape: "normal", keycode: 25 },
|
||||
{ keytype: "normal", label: "х", labelShift: "Х", shape: "normal", keycode: 26 },
|
||||
{ keytype: "normal", label: "ъ", labelShift: "Ъ", shape: "normal", keycode: 27 },
|
||||
{ keytype: "normal", label: "\\", labelShift: "/", shape: "expand", keycode: 43 }
|
||||
],
|
||||
[
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "spacer", label: "", shape: "empty" },
|
||||
{ keytype: "normal", label: "ф", labelShift: "Ф", shape: "normal", keycode: 30 },
|
||||
{ keytype: "normal", label: "ы", labelShift: "Ы", shape: "normal", keycode: 31 },
|
||||
{ keytype: "normal", label: "в", labelShift: "В", shape: "normal", keycode: 32 },
|
||||
{ keytype: "normal", label: "а", labelShift: "А", shape: "normal", keycode: 33 },
|
||||
{ keytype: "normal", label: "п", labelShift: "П", shape: "normal", keycode: 34 },
|
||||
{ keytype: "normal", label: "р", labelShift: "Р", shape: "normal", keycode: 35 },
|
||||
{ keytype: "normal", label: "о", labelShift: "О", shape: "normal", keycode: 36 },
|
||||
{ keytype: "normal", label: "л", labelShift: "Л", shape: "normal", keycode: 37 },
|
||||
{ keytype: "normal", label: "д", labelShift: "Д", shape: "normal", keycode: 38 },
|
||||
{ keytype: "normal", label: "ж", labelShift: "Ж", shape: "normal", keycode: 39 },
|
||||
{ keytype: "normal", label: "э", labelShift: "Э", shape: "normal", keycode: 40 },
|
||||
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Shift", shape: "shift", keycode: 42 },
|
||||
{ keytype: "normal", label: "я", labelShift: "Я", shape: "normal", keycode: 44 },
|
||||
{ keytype: "normal", label: "ч", labelShift: "Ч", shape: "normal", keycode: 45 },
|
||||
{ keytype: "normal", label: "с", labelShift: "С", shape: "normal", keycode: 46 },
|
||||
{ keytype: "normal", label: "м", labelShift: "М", shape: "normal", keycode: 47 },
|
||||
{ keytype: "normal", label: "и", labelShift: "И", shape: "normal", keycode: 48 },
|
||||
{ keytype: "normal", label: "т", labelShift: "Т", shape: "normal", keycode: 49 },
|
||||
{ keytype: "normal", label: "ь", labelShift: "Ь", shape: "normal", keycode: 50 },
|
||||
{ keytype: "normal", label: "б", labelShift: "Б", shape: "normal", keycode: 51 },
|
||||
{ keytype: "normal", label: "ю", labelShift: "Ю", shape: "normal", keycode: 52 },
|
||||
{ keytype: "normal", label: ".", labelShift: ",", shape: "normal", keycode: 53 },
|
||||
{ keytype: "modkey", label: "Shift", shape: "expand", keycode: 54 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
|
||||
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
|
||||
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
|
||||
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
|
||||
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
|
||||
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property Component regionComponent: Component {
|
||||
Region {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overlayLoader
|
||||
active: GlobalStates.overlayOpen || OverlayContext.hasPinnedWidgets
|
||||
sourceComponent: PanelWindow {
|
||||
id: overlayWindow
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "quickshell:overlay"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
// Use OnDemand for pinned widgets to allow focus switching with mouse clicks
|
||||
WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : (OverlayContext.clickableWidgets.length > 0 ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None)
|
||||
visible: true
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {
|
||||
item: GlobalStates.overlayOpen ? overlayContent : null
|
||||
regions: OverlayContext.clickableWidgets.map((widget) => regionComponent.createObject(this, {
|
||||
item: widget
|
||||
}));
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [overlayWindow]
|
||||
active: false
|
||||
onCleared: () => {
|
||||
if (!active) GlobalStates.overlayOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onOverlayOpenChanged() {
|
||||
delayedGrabTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedGrabTimer
|
||||
interval: Appearance.animation.elementMoveFast.duration
|
||||
onTriggered: {
|
||||
grab.active = GlobalStates.overlayOpen;
|
||||
}
|
||||
}
|
||||
|
||||
OverlayContent {
|
||||
id: overlayContent
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "overlay"
|
||||
|
||||
function toggle(): void {
|
||||
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "overlayToggle"
|
||||
description: "Toggles overlay on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import qs.modules.common
|
||||
|
||||
Rectangle {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
color: Appearance.colors.colSurfaceContainer
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.widgets.widgetCanvas
|
||||
|
||||
Item {
|
||||
id: root
|
||||
focus: true
|
||||
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
|
||||
|
||||
Keys.onPressed: (event) => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
GlobalStates.overlayOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
property real initScale: Config.options.overlay.openingZoomAnimation ? 1.08 : 1.000001
|
||||
scale: initScale
|
||||
Component.onCompleted: {
|
||||
scale = 1
|
||||
}
|
||||
Behavior on scale {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
color: Appearance.colors.colScrim
|
||||
visible: Config.options.overlay.darkenScreen && opacity > 0
|
||||
opacity: (GlobalStates.overlayOpen && root.scale !== initScale) ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
WidgetCanvas {
|
||||
anchors.fill: parent
|
||||
onClicked: GlobalStates.overlayOpen = false
|
||||
|
||||
OverlayTaskbar {
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
topMargin: 50
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Persistent.states.overlay.open.map(identifier => {
|
||||
return OverlayContext.availableWidgets.find(w => w.identifier === identifier);
|
||||
})
|
||||
objectProp: "identifier"
|
||||
}
|
||||
delegate: OverlayWidgetDelegateChooser {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<var> availableWidgets: [
|
||||
{ identifier: "crosshair", materialSymbol: "point_scan" },
|
||||
{ identifier: "fpsLimiter", materialSymbol: "animation" },
|
||||
{ identifier: "floatingImage", materialSymbol: "imagesmode" },
|
||||
{ identifier: "recorder", materialSymbol: "screen_record" },
|
||||
{ identifier: "resources", materialSymbol: "browse_activity" },
|
||||
{ identifier: "stickypad", materialSymbol: "note_stack" },
|
||||
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
|
||||
]
|
||||
|
||||
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
|
||||
|
||||
property list<string> pinnedWidgetIdentifiers: []
|
||||
property list<var> clickableWidgets: []
|
||||
|
||||
function pin(identifier: string, pin = true) {
|
||||
if (pin) {
|
||||
if (!root.pinnedWidgetIdentifiers.includes(identifier)) {
|
||||
root.pinnedWidgetIdentifiers.push(identifier)
|
||||
}
|
||||
} else {
|
||||
root.pinnedWidgetIdentifiers = root.pinnedWidgetIdentifiers.filter(id => id !== identifier)
|
||||
}
|
||||
}
|
||||
|
||||
function registerClickableWidget(widget: var, clickable = true) {
|
||||
if (clickable) {
|
||||
if (!root.clickableWidgets.includes(widget)) {
|
||||
root.clickableWidgets.push(widget)
|
||||
}
|
||||
} else {
|
||||
root.clickableWidgets = root.clickableWidgets.filter(w => w !== widget)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.widgets.widgetCanvas
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real padding: 8
|
||||
|
||||
opacity: GlobalStates.overlayOpen ? 1 : 0
|
||||
implicitWidth: contentRow.implicitWidth + (padding * 2)
|
||||
implicitHeight: contentRow.implicitHeight + (padding * 2)
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Appearance.rounding.large
|
||||
border.color: Appearance.colors.colOutlineVariant
|
||||
border.width: 1
|
||||
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.padding
|
||||
}
|
||||
spacing: 6
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: OverlayContext.availableWidgets
|
||||
}
|
||||
delegate: WidgetButton {
|
||||
required property var modelData
|
||||
identifier: modelData.identifier
|
||||
materialSymbol: modelData.materialSymbol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Separator {}
|
||||
TimeWidget {}
|
||||
Separator {
|
||||
visible: Battery.available
|
||||
}
|
||||
BatteryWidget {
|
||||
visible: Battery.available
|
||||
}
|
||||
}
|
||||
|
||||
component Separator: Rectangle {
|
||||
implicitWidth: 1
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: 10
|
||||
Layout.bottomMargin: 10
|
||||
}
|
||||
|
||||
component TimeWidget: StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 6
|
||||
|
||||
text: DateTime.time
|
||||
color: Appearance.colors.colOnSurface
|
||||
font {
|
||||
family: Appearance.font.family.numbers
|
||||
variableAxes: Appearance.font.variableAxes.numbers
|
||||
pixelSize: 22
|
||||
}
|
||||
}
|
||||
|
||||
component BatteryWidget: Row {
|
||||
id: batteryWidget
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: 6
|
||||
Layout.rightMargin: 6
|
||||
spacing: 2
|
||||
property color colText: Battery.isLowAndNotCharging ? Appearance.colors.colError : Appearance.colors.colOnSurface
|
||||
|
||||
MaterialSymbol {
|
||||
id: boltIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fill: 1
|
||||
text: Battery.isCharging ? "bolt" : "battery_android_full"
|
||||
color: batteryWidget.colText
|
||||
iconSize: 24
|
||||
animateChange: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: batteryText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Math.round(Battery.percentage * 100) + "%"
|
||||
color: batteryWidget.colText
|
||||
font {
|
||||
family: Appearance.font.family.numbers
|
||||
variableAxes: Appearance.font.variableAxes.numbers
|
||||
pixelSize: 18
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WidgetButton: RippleButton {
|
||||
id: widgetButton
|
||||
required property string identifier
|
||||
required property string materialSymbol
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
toggled: Persistent.states.overlay.open.includes(identifier)
|
||||
onClicked: {
|
||||
if (widgetButton.toggled) {
|
||||
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== identifier);
|
||||
} else {
|
||||
Persistent.states.overlay.open.push(identifier);
|
||||
}
|
||||
}
|
||||
implicitWidth: implicitHeight
|
||||
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
buttonRadius: root.radius - (root.height - height) / 2
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
MaterialSymbol {
|
||||
id: iconWidget
|
||||
anchors.centerIn: parent
|
||||
iconSize: 24
|
||||
text: widgetButton.materialSymbol
|
||||
color: widgetButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import qs.modules.ii.overlay.crosshair
|
||||
import qs.modules.ii.overlay.volumeMixer
|
||||
import qs.modules.ii.overlay.floatingImage
|
||||
import qs.modules.ii.overlay.fpsLimiter
|
||||
import qs.modules.ii.overlay.recorder
|
||||
import qs.modules.ii.overlay.resources
|
||||
import qs.modules.ii.overlay.stickypad
|
||||
|
||||
DelegateChooser {
|
||||
id: root
|
||||
role: "identifier"
|
||||
|
||||
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
|
||||
DelegateChoice { roleValue: "floatingImage"; FloatingImage {} }
|
||||
DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} }
|
||||
DelegateChoice { roleValue: "recorder"; Recorder {} }
|
||||
DelegateChoice { roleValue: "resources"; Resources {} }
|
||||
DelegateChoice { roleValue: "stickypad"; Stickypad {} }
|
||||
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.widgets.widgetCanvas
|
||||
|
||||
/*
|
||||
* To make an overlay widget:
|
||||
* 1. Create a modules/overlay/<yourWidget>/<YourWidget>.qml, using this as the base class and declare your widget content as contentItem
|
||||
* 2. Add an entry to OverlayContext.availableWidgets with identifier=<yourWidgetIdentifier>
|
||||
* 3. Add an entry in Persistent.states.overlay.<yourWidgetIdentifier> with x, y, width, height, pinned, clickthrough properties set to reasonable defaults
|
||||
* 4. Add an entry in OverlayWidgetDelegateChooser with roleValue=<yourWidgetIdentifier> and Declare your widget in there
|
||||
* Use existing entries as reference.
|
||||
*/
|
||||
AbstractOverlayWidget {
|
||||
id: root
|
||||
|
||||
// To be defined by subclasses
|
||||
required property Item contentItem
|
||||
property bool fancyBorders: true
|
||||
property bool showCenterButton: false
|
||||
property bool showClickabilityButton: true
|
||||
|
||||
// Defaults n stuff
|
||||
required property var modelData
|
||||
readonly property string identifier: modelData.identifier
|
||||
readonly property string materialSymbol: modelData.materialSymbol ?? "widgets"
|
||||
property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); })
|
||||
property var persistentStateEntry: Persistent.states.overlay[identifier]
|
||||
property real radius: Appearance.rounding.windowRounding
|
||||
property real minimumWidth: contentItem.implicitWidth
|
||||
property real minimumHeight: contentItem.implicitHeight
|
||||
property real resizeMargin: 8
|
||||
property real padding: 6
|
||||
property real contentRadius: radius - padding
|
||||
|
||||
// Resizing
|
||||
function getXResizeDirection(x) {
|
||||
return (x < root.resizeMargin) ? -1 : (x > root.width - root.resizeMargin) ? 1 : 0
|
||||
}
|
||||
function getYResizeDirection(y) {
|
||||
return (y < root.resizeMargin) ? -1 : (y > root.height - root.resizeMargin) ? 1 : 0
|
||||
}
|
||||
hoverEnabled: true
|
||||
property bool resizable: true
|
||||
property bool resizing: false
|
||||
property int resizeXDirection: getXResizeDirection(mouseX)
|
||||
property int resizeYDirection: getYResizeDirection(mouseY)
|
||||
draggable: GlobalStates.overlayOpen
|
||||
drag.target: undefined
|
||||
animateXPos: !dragHandler.active
|
||||
animateYPos: !dragHandler.active
|
||||
z: dragHandler.active ? 2 : 1
|
||||
cursorShape: {
|
||||
if (dragHandler.active) return root.resizing ? cursorShape : Qt.ArrowCursor;
|
||||
if (resizeMargin < mouseX && mouseX < width - resizeMargin &&
|
||||
resizeMargin < mouseY && mouseY < height - resizeMargin) {
|
||||
return Qt.ArrowCursor;
|
||||
} else {
|
||||
if (!root.resizable) return Qt.ArrowCursor;
|
||||
const dragIsLeft = mouseX < width / 2
|
||||
const dragIsTop = mouseY < height / 2
|
||||
if ((dragIsLeft && dragIsTop) || (!dragIsLeft && !dragIsTop)) {
|
||||
return Qt.SizeFDiagCursor
|
||||
} else {
|
||||
return Qt.SizeBDiagCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Positioning & sizing
|
||||
x: Math.round(persistentStateEntry.x) // Round or it'll be blurry
|
||||
y: Math.round(persistentStateEntry.y) // Round or it'll be blurry
|
||||
pinned: persistentStateEntry.pinned
|
||||
clickthrough: persistentStateEntry.clickthrough
|
||||
drag {
|
||||
minimumX: 0
|
||||
minimumY: 0
|
||||
maximumX: root.parent?.width - root.width
|
||||
maximumY: root.parent?.height - root.height
|
||||
}
|
||||
opacity: (GlobalStates.overlayOpen || !clickthrough) ? 1.0 : Config.options.overlay.clickthroughOpacity
|
||||
|
||||
// Guarded states & registration funcs
|
||||
property bool open: Persistent.states.overlay.open
|
||||
property bool actuallyPinned: pinned && open
|
||||
property bool actuallyClickable: !clickthrough && actuallyPinned && open
|
||||
onActuallyPinnedChanged: reportPinnedState();
|
||||
onActuallyClickableChanged: reportClickableState();
|
||||
function reportPinnedState() {
|
||||
OverlayContext.pin(identifier, actuallyPinned);
|
||||
}
|
||||
function reportClickableState() {
|
||||
OverlayContext.registerClickableWidget(contentItem, actuallyClickable);
|
||||
}
|
||||
|
||||
// Self-registeration with OverlayContext
|
||||
Component.onCompleted: {
|
||||
reportPinnedState();
|
||||
reportClickableState();
|
||||
}
|
||||
|
||||
// Hooks
|
||||
onPressed: (event) => {
|
||||
// We're only interested in handling resize here
|
||||
// Early returns
|
||||
if (!root.resizable) return;
|
||||
if (root.resizeMargin < event.x && event.x < root.width - root.resizeMargin &&
|
||||
root.resizeMargin < event.y && event.y < root.height - root.resizeMargin) {
|
||||
return;
|
||||
}
|
||||
// Resizing setup
|
||||
root.resizing = true;
|
||||
root.resizeXDirection = getXResizeDirection(event.x);
|
||||
root.resizeYDirection = getYResizeDirection(event.y);
|
||||
if (root.resizeYDirection !== 0 && root.resizeXDirection === 0) {
|
||||
root.resizeXDirection = event.x < root.width / 2 ? -1 : 1;
|
||||
} else if (root.resizeXDirection !== 0 && root.resizeYDirection === 0) {
|
||||
root.resizeYDirection = event.y < root.height / 2 ? -1 : 1;
|
||||
}
|
||||
}
|
||||
onPositionChanged: (event) => {
|
||||
if (!resizing) return;
|
||||
contentContainer.implicitWidth = Math.max(root.persistentStateEntry.width + dragHandler.xAxis.activeValue * root.resizeXDirection, root.minimumWidth);
|
||||
contentContainer.implicitHeight = Math.max(root.persistentStateEntry.height + dragHandler.yAxis.activeValue * root.resizeYDirection, root.minimumHeight);
|
||||
const negativeXDrag = root.resizeXDirection === -1;
|
||||
const negativeYDrag = root.resizeYDirection === -1;
|
||||
const wantedX = root.persistentStateEntry.x + (negativeXDrag ? dragHandler.xAxis.activeValue : 0)
|
||||
const wantedY = root.persistentStateEntry.y + (negativeYDrag ? dragHandler.yAxis.activeValue : 0)
|
||||
const negativeXDragLimit = root.persistentStateEntry.x + root.persistentStateEntry.width - contentContainer.implicitWidth;
|
||||
const negativeYDragLimit = root.persistentStateEntry.y + root.persistentStateEntry.height - contentContainer.implicitHeight;
|
||||
root.x = negativeXDrag ? Math.min(wantedX, negativeXDragLimit) : wantedX;
|
||||
root.y = negativeYDrag ? Math.min(wantedY, negativeYDragLimit) : wantedY;
|
||||
}
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
target: (root.draggable && !root.resizing) ? root : null
|
||||
onActiveChanged: { // Handle drag release
|
||||
if (!active) {
|
||||
root.resizing = false;
|
||||
root.savePosition();
|
||||
}
|
||||
}
|
||||
xAxis.minimum: 0
|
||||
xAxis.maximum: root.parent?.width - root.width
|
||||
yAxis.minimum: 0
|
||||
yAxis.maximum: root.parent?.height - root.height
|
||||
}
|
||||
|
||||
function close() {
|
||||
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier);
|
||||
}
|
||||
|
||||
function togglePinned() {
|
||||
persistentStateEntry.pinned = !persistentStateEntry.pinned;
|
||||
}
|
||||
|
||||
function toggleClickthrough() {
|
||||
persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough;
|
||||
}
|
||||
|
||||
function savePosition(xPos = root.x, yPos = root.y, width = contentContainer.implicitWidth, height = contentContainer.implicitHeight) {
|
||||
persistentStateEntry.x = Math.round(xPos);
|
||||
persistentStateEntry.y = Math.round(yPos);
|
||||
persistentStateEntry.width = Math.round(width);
|
||||
persistentStateEntry.height = Math.round(height);
|
||||
}
|
||||
|
||||
function center() {
|
||||
const targetX = (root.parent.width - contentColumn.width) / 2 - root.resizeMargin
|
||||
const targetY = (root.parent.height - contentContainer.height) / 2 - titleBar.implicitHeight + border.border.width - root.resizeMargin
|
||||
root.x = targetX
|
||||
root.y = targetY
|
||||
root.savePosition(targetX, targetY)
|
||||
}
|
||||
|
||||
visible: GlobalStates.overlayOpen || actuallyPinned
|
||||
implicitWidth: contentColumn.implicitWidth + resizeMargin * 2
|
||||
implicitHeight: contentColumn.implicitHeight + resizeMargin * 2
|
||||
|
||||
Rectangle {
|
||||
id: border
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.resizeMargin
|
||||
}
|
||||
color: ColorUtils.transparentize(Appearance.colors.colLayer1, (root.fancyBorders && GlobalStates.overlayOpen) ? 0 : 1)
|
||||
radius: root.radius
|
||||
border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1)
|
||||
border.width: 1
|
||||
|
||||
layer.enabled: GlobalStates.overlayOpen
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: border.width
|
||||
height: border.height
|
||||
radius: root.radius
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
z: root.fancyBorders ? 0 : -1
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Title bar
|
||||
Rectangle {
|
||||
id: titleBar
|
||||
opacity: GlobalStates.overlayOpen ? 1 : 0
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: titleBarRow.implicitWidth + root.padding * 2
|
||||
implicitHeight: titleBarRow.implicitHeight + root.padding * 2
|
||||
color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1
|
||||
// border.color: Appearance.colors.colOutlineVariant
|
||||
// border.width: 1
|
||||
|
||||
RowLayout {
|
||||
id: titleBarRow
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.padding
|
||||
}
|
||||
spacing: 2
|
||||
|
||||
MaterialSymbol {
|
||||
text: root.materialSymbol
|
||||
Layout.leftMargin: 6
|
||||
iconSize: 20
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: 4
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: root.title
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
TitlebarButton {
|
||||
visible: root.showCenterButton
|
||||
materialSymbol: "recenter"
|
||||
onClicked: root.center()
|
||||
StyledToolTip {
|
||||
text: "Center"
|
||||
}
|
||||
}
|
||||
|
||||
TitlebarButton {
|
||||
visible: (root.pinned && root.showClickabilityButton)
|
||||
materialSymbol: "mouse"
|
||||
toggled: !root.clickthrough
|
||||
onClicked: root.toggleClickthrough()
|
||||
StyledToolTip {
|
||||
text: "Clickable when pinned"
|
||||
}
|
||||
}
|
||||
|
||||
TitlebarButton {
|
||||
materialSymbol: "keep"
|
||||
toggled: root.pinned
|
||||
onClicked: root.togglePinned()
|
||||
StyledToolTip {
|
||||
text: "Pin"
|
||||
}
|
||||
}
|
||||
|
||||
TitlebarButton {
|
||||
materialSymbol: "close"
|
||||
onClicked: root.close()
|
||||
StyledToolTip {
|
||||
text: "Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
Item {
|
||||
id: contentContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: root.fancyBorders ? root.padding : 0
|
||||
Layout.topMargin: -border.border.width // Border of a rectangle is drawn inside its bounds, so we do this to make the gap not too big
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
implicitWidth: Math.max(root.persistentStateEntry.width, root.minimumWidth)
|
||||
implicitHeight: Math.max(root.persistentStateEntry.height, root.minimumHeight)
|
||||
children: [root.contentItem]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
component TitlebarButton: RippleButton {
|
||||
id: titlebarButton
|
||||
required property string materialSymbol
|
||||
buttonRadius: height / 2
|
||||
implicitHeight: contentItem.implicitHeight
|
||||
implicitWidth: implicitHeight
|
||||
padding: 0
|
||||
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 30
|
||||
implicitHeight: 30
|
||||
|
||||
MaterialSymbol {
|
||||
id: iconWidget
|
||||
anchors.centerIn: parent
|
||||
iconSize: 20
|
||||
text: titlebarButton.materialSymbol
|
||||
fill: titlebarButton.toggled
|
||||
color: titlebarButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
StyledOverlayWidget {
|
||||
id: root
|
||||
fancyBorders: false // Crosshair should be see-through
|
||||
showCenterButton: true
|
||||
opacity: 1 // The crosshair itself already has transparency if configured
|
||||
showClickabilityButton: false
|
||||
clickthrough: true
|
||||
resizable: false
|
||||
|
||||
contentItem: CrosshairContent {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Keys to props
|
||||
// f, 0f, 1f, m are irrelevant as they're firing error stuff
|
||||
// 0 is irrelevant because it's some profile stuff
|
||||
property var propertyMap: ({
|
||||
"c": "color",
|
||||
"u": "colorCode",
|
||||
"h": "outline",
|
||||
"o": "outlineOpacity",
|
||||
"t": "outlineThickness",
|
||||
"d": "centerDot",
|
||||
"a": "centerDotOpacity",
|
||||
"z": "centerDotSize",
|
||||
"0a": "innerLineOpacity",
|
||||
"0l": "innerLineLength",
|
||||
"0v": "innerLineVerticalLength",
|
||||
"0g": "innerLineUnbindAxesLengths",
|
||||
"0t": "innerLineThickness",
|
||||
"0o": "innerLineOffset",
|
||||
"1b": "outerLines",
|
||||
"1a": "outerLineOpacity",
|
||||
"1l": "outerLineLength",
|
||||
"1v": "outerLineVerticalLength",
|
||||
"1g": "outerLineUnbindAxesLengths",
|
||||
"1t": "outerLineThickness",
|
||||
"1o": "outerLineOffset",
|
||||
})
|
||||
property var colorMap: ({
|
||||
0: "#FFFFFF",
|
||||
1: "#00FF00",
|
||||
2: "#7FFF00",
|
||||
3: "#DFFF00",
|
||||
4: "#FFFF00",
|
||||
5: "#00FFFF",
|
||||
6: "#FF00FF",
|
||||
7: "#FF0000"
|
||||
})
|
||||
|
||||
// Raw props
|
||||
property int color: 0
|
||||
property string colorCode: "#FFFFFF"
|
||||
property bool outline: true
|
||||
property real outlineOpacity: 0.5
|
||||
property int outlineThickness: 1
|
||||
property bool centerDot: false
|
||||
property real centerDotOpacity: 1
|
||||
property int centerDotSize: 2
|
||||
property bool innerLines: true
|
||||
property real innerLineOpacity: 0.8
|
||||
property int innerLineLength: 6
|
||||
property int innerLineVerticalLength: innerLineLength
|
||||
property bool innerLineUnbindAxesLengths: false
|
||||
property int innerLineThickness: 2
|
||||
property int innerLineOffset: 3
|
||||
property bool outerLines: true
|
||||
property real outerLineOpacity: 0.35
|
||||
property int outerLineLength: 2
|
||||
property int outerLineVerticalLength: outerLineLength
|
||||
property bool outerLineUnbindAxesLengths: false
|
||||
property int outerLineThickness: 2
|
||||
property int outerLineOffset: 10
|
||||
property string defaultCode: "c;0;u;FFFFFF;h;1;o;0.5;t;1;d;0;a;1;z;2;0a;0.8;0l;6;0v;6;0g;0;0t;2;0o;3;1b;1;1a;0.35;1l;2;1v;2;1g;0;1t;2;1o;10"
|
||||
|
||||
function loadFromCode(code: string): void {
|
||||
let args = code.split(";");
|
||||
for (let i = 0; i < args.length; i+= 2) {
|
||||
let key = args[i];
|
||||
let value = args[i+1];
|
||||
let targetKey = root.propertyMap[key];
|
||||
let targetType = typeof root[targetKey];
|
||||
|
||||
if (targetKey === undefined) continue;
|
||||
|
||||
if (targetType === "number") {
|
||||
value = parseFloat(value);
|
||||
} else if (targetType === "boolean") {
|
||||
value = (value === "1");
|
||||
}
|
||||
if (targetKey === "colorCode") {
|
||||
value = "#" + value.slice(0, 6);
|
||||
}
|
||||
root[targetKey] = value;
|
||||
}
|
||||
|
||||
if (!root.innerLineUnbindAxesLengths) {
|
||||
root.innerLineVerticalLength = root.innerLineLength;
|
||||
}
|
||||
if (!root.outerLineUnbindAxesLengths) {
|
||||
root.outerLineVerticalLength = root.outerLineLength;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update values from code
|
||||
property var code: Config.options.crosshair.code
|
||||
Component.onCompleted: reloadFromCode();
|
||||
onCodeChanged: reloadFromCode();
|
||||
function reloadFromCode() {
|
||||
root.loadFromCode(root.defaultCode);
|
||||
root.loadFromCode(root.code);
|
||||
}
|
||||
|
||||
// Aggregated props
|
||||
property color crosshairColor: {
|
||||
if (colorMap[color] !== undefined) return root.colorMap[color];
|
||||
if (color === 8) return colorCode;
|
||||
return "#FFFFFF";
|
||||
}
|
||||
property int borderWidth: outline ? outlineThickness : 0
|
||||
property color borderColor: ColorUtils.transparentize("black", 1 - root.outlineOpacity)
|
||||
property color innerLineColor: ColorUtils.transparentize(root.crosshairColor, 1 - root.innerLineOpacity)
|
||||
property color outerLineColor: ColorUtils.transparentize(root.crosshairColor, 1 - root.outerLineOpacity)
|
||||
property int innerLineTotalOffset: root.centerDotSize / 2 + 1 + root.innerLineOffset
|
||||
property int outerLineTotalOffset: root.centerDotSize / 2 + 1 + root.outerLineOffset
|
||||
property real centerDotTotalSize: root.centerDotSize + root.borderWidth * 2
|
||||
property real innerLineTotalSize: (innerLineTotalOffset + root.innerLineLength + root.borderWidth) * 2
|
||||
property real outerLineTotalSize: (outerLineTotalOffset + root.outerLineLength + root.borderWidth) * 2
|
||||
implicitWidth: Math.max(centerDotTotalSize, innerLineTotalSize, outerLineTotalSize) + 2 // 2 for pixel correction
|
||||
implicitHeight: implicitWidth
|
||||
// width: implicitWidth
|
||||
// height: implicitHeight
|
||||
|
||||
Rectangle {
|
||||
id: centerDot
|
||||
visible: root.centerDot
|
||||
anchors.centerIn: parent
|
||||
|
||||
color: root.crosshairColor
|
||||
opacity: root.centerDotOpacity
|
||||
width: centerDotTotalSize
|
||||
height: width
|
||||
|
||||
border.width: root.borderWidth
|
||||
border.color: root.borderColor
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: innerLines
|
||||
model: 4
|
||||
Item {
|
||||
id: innerHair
|
||||
z: index % 2 // Vertical lines above horizontal lines
|
||||
required property int index
|
||||
property int pixelCorrection: (root.innerLineThickness % 2 === 1 && index > 1) ? 1 : 0
|
||||
property int hairLength: (innerHair.index % 2 === 0 ? root.innerLineLength : root.innerLineVerticalLength)
|
||||
visible: root.innerLines && hairLength > 0
|
||||
anchors.fill: parent
|
||||
rotation: index * 90
|
||||
Rectangle {
|
||||
x: parent.width / 2 + root.innerLineTotalOffset - root.borderWidth + innerHair.pixelCorrection
|
||||
y: parent.height / 2 - height / 2
|
||||
|
||||
color: root.innerLineColor
|
||||
width: innerHair.hairLength + root.borderWidth * 2
|
||||
height: root.innerLineThickness + root.borderWidth * 2
|
||||
|
||||
border.width: root.borderWidth
|
||||
border.color: root.borderColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: outerLines
|
||||
model: 4
|
||||
Item {
|
||||
id: outerHair
|
||||
z: index % 2 + 2 // Vertical lines above horizontal lines, above inner lines
|
||||
required property int index
|
||||
property int pixelCorrection: (root.outerLineThickness % 2 === 1 && index > 1) ? 1 : 0
|
||||
property int hairLength: (outerHair.index % 2 === 0 ? root.outerLineLength : root.outerLineVerticalLength)
|
||||
visible: root.outerLines && hairLength > 0
|
||||
anchors.fill: parent
|
||||
rotation: index * 90
|
||||
Rectangle {
|
||||
x: parent.width / 2 + root.outerLineTotalOffset - root.borderWidth + outerHair.pixelCorrection
|
||||
y: parent.height / 2 - height / 2
|
||||
|
||||
color: root.outerLineColor
|
||||
width: hairLength + root.borderWidth * 2
|
||||
height: root.outerLineThickness + root.borderWidth * 2
|
||||
|
||||
border.width: root.borderWidth
|
||||
border.color: root.borderColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.utils
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
StyledOverlayWidget {
|
||||
id: root
|
||||
showClickabilityButton: false
|
||||
resizable: false
|
||||
clickthrough: true
|
||||
|
||||
property string imageSource: Config.options.overlay.floatingImage.imageSource
|
||||
property real scaleFactor: Config.options.overlay.floatingImage.scale
|
||||
property int imageWidth: 0
|
||||
property int imageHeight: 0
|
||||
|
||||
// Override to always save 0 size
|
||||
function savePosition(xPos = root.x, yPos = root.y, width = 0, height = 0) {
|
||||
root.persistentStateEntry.x = Math.round(xPos);
|
||||
root.persistentStateEntry.y = Math.round(yPos);
|
||||
root.persistentStateEntry.width = 0
|
||||
root.persistentStateEntry.height = 0
|
||||
}
|
||||
|
||||
onImageSourceChanged: {
|
||||
imageDownloader.running = false;
|
||||
imageDownloader.sourceUrl = root.imageSource;
|
||||
imageDownloader.filePath = Qt.resolvedUrl(Directories.tempImages + "/" + Qt.md5(root.imageSource))
|
||||
imageDownloader.running = true;
|
||||
}
|
||||
onScaleFactorChanged: {
|
||||
setSize();
|
||||
}
|
||||
|
||||
function setSize() {
|
||||
bg.implicitWidth = root.imageWidth * root.scaleFactor;
|
||||
bg.implicitHeight = root.imageHeight * root.scaleFactor;
|
||||
}
|
||||
|
||||
contentItem: OverlayBackground {
|
||||
id: bg
|
||||
color: ColorUtils.transparentize(Appearance.m3colors.m3surfaceContainer, root.actuallyPinned ? 1 : 0)
|
||||
radius: root.contentRadius
|
||||
|
||||
WheelHandler {
|
||||
onWheel: (event) => {
|
||||
if (event.angleDelta.y < 0) {
|
||||
Config.options.overlay.floatingImage.scale = Math.max(0.1, Config.options.overlay.floatingImage.scale - 0.1);
|
||||
}
|
||||
else if (event.angleDelta.y > 0) {
|
||||
Config.options.overlay.floatingImage.scale = Math.min(5.0, Config.options.overlay.floatingImage.scale + 0.1);
|
||||
}
|
||||
}
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: bg.width
|
||||
height: bg.height
|
||||
radius: bg.radius
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
id: animatedImage
|
||||
anchors.centerIn: parent
|
||||
width: root.imageWidth * root.scaleFactor
|
||||
height: root.imageHeight * root.scaleFactor
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
||||
playing: visible
|
||||
asynchronous: true
|
||||
source: ""
|
||||
|
||||
ImageDownloaderProcess {
|
||||
id: imageDownloader
|
||||
filePath: Qt.resolvedUrl(Directories.tempImages + "/" + Qt.md5(root.imageSource))
|
||||
sourceUrl: root.imageSource
|
||||
|
||||
onDone: (path, width, height) => {
|
||||
root.imageWidth = width;
|
||||
root.imageHeight = height;
|
||||
root.setSize();
|
||||
animatedImage.source = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
StyledOverlayWidget {
|
||||
id: root
|
||||
title: "MangoHud FPS"
|
||||
minimumWidth: 275
|
||||
minimumHeight: 100
|
||||
contentItem: FpsLimiterContent {
|
||||
radius: root.contentRadius
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
OverlayBackground {
|
||||
id: root
|
||||
|
||||
enum State { Normal, Success, Error }
|
||||
|
||||
property real padding: 16
|
||||
property var currentState: FpsLimiterContent.State.Normal
|
||||
implicitWidth: content.implicitWidth + (padding * 2)
|
||||
implicitHeight: content.implicitHeight + (padding * 2)
|
||||
|
||||
Timer {
|
||||
id: iconResetTimer
|
||||
interval: 1000
|
||||
onTriggered: {
|
||||
root.currentState = FpsLimiterContent.State.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
function applyLimit() {
|
||||
var fpsValue = parseInt(fpsField.text);
|
||||
if (isNaN(fpsValue) || fpsValue < 0) {
|
||||
root.currentState = FpsLimiterContent.State.Error;
|
||||
iconResetTimer.restart();
|
||||
fpsField.text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var cfgPaths = [
|
||||
"~/.config/MangoHud/MangoHud.conf",
|
||||
]; // MangoHud config files
|
||||
|
||||
var updateCommands = cfgPaths.map(path => {
|
||||
return "if grep -q '^fps_limit=' " + path + "; " +
|
||||
"then sed -i 's/^fps_limit=.*/fps_limit=" + fpsValue + "/' " + path + "; " +
|
||||
"else echo 'fps_limit=" + fpsValue + "' >> " + path + "; fi";
|
||||
}).join("; ");
|
||||
|
||||
var cmd = updateCommands + "; pkill -SIGUSR2 mangohud";
|
||||
|
||||
fpsSetter.command = ["bash", "-c", cmd];
|
||||
fpsSetter.startDetached();
|
||||
|
||||
root.currentState = FpsLimiterContent.State.Success;
|
||||
iconResetTimer.restart();
|
||||
|
||||
// Clear the field after applying
|
||||
fpsField.text = "";
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fpsSetter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
ToolbarTextField {
|
||||
id: fpsField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 200
|
||||
placeholderText: root.currentState === FpsLimiterContent.State.Error ? Translation.tr("Enter a valid number") : Translation.tr("Set FPS limit")
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
focus: true
|
||||
|
||||
onAccepted: {
|
||||
root.applyLimit();
|
||||
}
|
||||
}
|
||||
|
||||
IconToolbarButton {
|
||||
id: applyButton
|
||||
text: switch (root.currentState) {
|
||||
case FpsLimiterContent.State.Error: return "close";
|
||||
case FpsLimiterContent.State.Success: return "check";
|
||||
case FpsLimiterContent.State.Normal:
|
||||
default: return "save";
|
||||
}
|
||||
enabled: root.currentState === FpsLimiterContent.State.Normal && fpsField.text.length > 0
|
||||
onClicked: {
|
||||
root.applyLimit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
StyledOverlayWidget {
|
||||
id: root
|
||||
minimumWidth: 310
|
||||
minimumHeight: 130
|
||||
|
||||
contentItem: OverlayBackground {
|
||||
id: contentItem
|
||||
radius: root.contentRadius
|
||||
property real padding: 8
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
spacing: 10
|
||||
|
||||
BigRecorderButton {
|
||||
materialSymbol: "screenshot_region"
|
||||
name: "Screenshot region"
|
||||
onClicked: {
|
||||
GlobalStates.overlayOpen = false;
|
||||
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
|
||||
}
|
||||
}
|
||||
|
||||
BigRecorderButton {
|
||||
materialSymbol: "photo_camera"
|
||||
name: "Screenshot"
|
||||
onClicked: {
|
||||
GlobalStates.overlayOpen = false;
|
||||
Quickshell.execDetached(["bash", "-c", "grim - | wl-copy"]);
|
||||
}
|
||||
}
|
||||
|
||||
BigRecorderButton {
|
||||
materialSymbol: "screen_record"
|
||||
name: "Record region"
|
||||
onClicked: {
|
||||
GlobalStates.overlayOpen = false;
|
||||
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "recordWithSound"]);
|
||||
}
|
||||
}
|
||||
|
||||
BigRecorderButton {
|
||||
materialSymbol: "capture"
|
||||
name: "Record screen"
|
||||
onClicked: {
|
||||
GlobalStates.overlayOpen = false;
|
||||
Quickshell.execDetached([Directories.recordScriptPath, "--fullscreen", "--sound"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RippleButton {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.fillWidth: false
|
||||
buttonRadius: height / 2
|
||||
colBackground: Appearance.colors.colLayer3
|
||||
colBackgroundHover: Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
onClicked: {
|
||||
GlobalStates.overlayOpen = false;
|
||||
Qt.openUrlExternally(`file://${Config.options.screenRecord.savePath}`);
|
||||
}
|
||||
contentItem: Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
MaterialSymbol {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "animated_images"
|
||||
iconSize: 20
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Open recordings folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BigRecorderButton: RippleButton {
|
||||
id: bigButton
|
||||
required property string materialSymbol
|
||||
required property string name
|
||||
implicitHeight: 66
|
||||
implicitWidth: 66
|
||||
buttonRadius: height / 2
|
||||
|
||||
colBackground: Appearance.colors.colLayer3
|
||||
colBackgroundHover: Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: bigButton.materialSymbol
|
||||
iconSize: 28
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
text: bigButton.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Qt.labs.synchronizer
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
StyledOverlayWidget {
|
||||
id: root
|
||||
minimumWidth: 300
|
||||
minimumHeight: 200
|
||||
property list<var> resources: [
|
||||
{
|
||||
"icon": "planner_review",
|
||||
"name": Translation.tr("CPU"),
|
||||
"history": ResourceUsage.cpuUsageHistory,
|
||||
"maxAvailableString": ResourceUsage.maxAvailableCpuString
|
||||
},
|
||||
{
|
||||
"icon": "memory",
|
||||
"name": Translation.tr("RAM"),
|
||||
"history": ResourceUsage.memoryUsageHistory,
|
||||
"maxAvailableString": ResourceUsage.maxAvailableMemoryString
|
||||
},
|
||||
{
|
||||
"icon": "swap_horiz",
|
||||
"name": Translation.tr("Swap"),
|
||||
"history": ResourceUsage.swapUsageHistory,
|
||||
"maxAvailableString": ResourceUsage.maxAvailableSwapString
|
||||
},
|
||||
]
|
||||
|
||||
contentItem: OverlayBackground {
|
||||
id: contentItem
|
||||
radius: root.contentRadius
|
||||
property real padding: 4
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: parent.padding
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
SecondaryTabBar {
|
||||
id: tabBar
|
||||
|
||||
currentIndex: Persistent.states.overlay.resources.tabIndex
|
||||
onCurrentIndexChanged: {
|
||||
Persistent.states.overlay.resources.tabIndex = tabBar.currentIndex;
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.resources.length
|
||||
delegate: SecondaryTabButton {
|
||||
required property int index
|
||||
property var modelData: root.resources[index]
|
||||
buttonIcon: modelData.icon
|
||||
buttonText: modelData.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResourceSummary {
|
||||
Layout.margins: 8
|
||||
history: root.resources[tabBar.currentIndex]?.history ?? []
|
||||
maxAvailableString: root.resources[tabBar.currentIndex]?.maxAvailableString ?? "--"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component ResourceSummary: RowLayout {
|
||||
id: resourceSummary
|
||||
required property list<real> history
|
||||
required property string maxAvailableString
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 12
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
StyledText {
|
||||
text: (resourceSummary.history[resourceSummary.history.length - 1] * 100).toFixed(1) + "%"
|
||||
font {
|
||||
family: Appearance.font.family.numbers
|
||||
variableAxes: Appearance.font.variableAxes.numbers
|
||||
pixelSize: Appearance.font.pixelSize.huge
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
text: Translation.tr("of %1").arg(resourceSummary.maxAvailableString)
|
||||
font {
|
||||
// family: Appearance.font.family.numbers
|
||||
// variableAxes: Appearance.font.variableAxes.numbers
|
||||
pixelSize: Appearance.font.pixelSize.smallie
|
||||
}
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: graphBg
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Appearance.rounding.small
|
||||
color: Appearance.colors.colSecondaryContainer
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: graphBg.width
|
||||
height: graphBg.height
|
||||
radius: graphBg.radius
|
||||
}
|
||||
}
|
||||
Graph {
|
||||
anchors.fill: parent
|
||||
values: root.resources[tabBar.currentIndex]?.history ?? []
|
||||
points: ResourceUsage.historyLength
|
||||
alignment: Graph.Alignment.Right
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.ii.overlay
|
||||
import qs.modules.ii.sidebarRight.volumeMixer
|
||||
|
||||
StyledOverlayWidget {
|
||||
id: root
|
||||
minimumWidth: 300
|
||||
minimumHeight: 380
|
||||
|
||||
contentItem: OverlayBackground {
|
||||
radius: root.contentRadius
|
||||
property real padding: 6
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: parent.padding
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
SecondaryTabBar {
|
||||
id: tabBar
|
||||
|
||||
currentIndex: Persistent.states.overlay.volumeMixer.tabIndex
|
||||
onCurrentIndexChanged: {
|
||||
Persistent.states.overlay.volumeMixer.tabIndex = tabBar.currentIndex;
|
||||
}
|
||||
|
||||
SecondaryTabButton {
|
||||
buttonIcon: "media_output"
|
||||
buttonText: Translation.tr("Output")
|
||||
}
|
||||
SecondaryTabButton {
|
||||
buttonIcon: "mic"
|
||||
buttonText: Translation.tr("Input")
|
||||
}
|
||||
}
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: Persistent.states.overlay.volumeMixer.tabIndex
|
||||
onCurrentIndexChanged: {
|
||||
Persistent.states.overlay.volumeMixer.tabIndex = swipeView.currentIndex;
|
||||
}
|
||||
clip: true
|
||||
|
||||
PaddedVolumeDialogContent {
|
||||
isSink: true
|
||||
}
|
||||
PaddedVolumeDialogContent {
|
||||
isSink: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component PaddedVolumeDialogContent: Item {
|
||||
id: paddedVolumeDialogContent
|
||||
property alias isSink: volDialogContent.isSink
|
||||
property real padding: 12
|
||||
implicitWidth: volDialogContent.implicitWidth + padding * 2
|
||||
implicitHeight: volDialogContent.implicitHeight + padding * 2
|
||||
|
||||
VolumeDialogContent {
|
||||
id: volDialogContent
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: paddedVolumeDialogContent.padding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import Qt.labs.synchronizer
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: overviewScope
|
||||
property bool dontAutoCancelSearch: false
|
||||
Variants {
|
||||
id: overviewVariants
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
id: root
|
||||
required property var modelData
|
||||
property string searchingText: ""
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
|
||||
screen: modelData
|
||||
visible: GlobalStates.overviewOpen
|
||||
|
||||
WlrLayershell.namespace: "quickshell:overview"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
// WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {
|
||||
item: GlobalStates.overviewOpen ? columnLayout : null
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [root]
|
||||
property bool canBeActive: root.monitorIsFocused
|
||||
active: false
|
||||
onCleared: () => {
|
||||
if (!active)
|
||||
GlobalStates.overviewOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onOverviewOpenChanged() {
|
||||
if (!GlobalStates.overviewOpen) {
|
||||
searchWidget.disableExpandAnimation();
|
||||
overviewScope.dontAutoCancelSearch = false;
|
||||
} else {
|
||||
if (!overviewScope.dontAutoCancelSearch) {
|
||||
searchWidget.cancelSearch();
|
||||
}
|
||||
delayedGrabTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedGrabTimer
|
||||
interval: Config.options.hacks.arbitraryRaceConditionDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!grab.canBeActive)
|
||||
return;
|
||||
grab.active = GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: columnLayout.implicitWidth
|
||||
implicitHeight: columnLayout.implicitHeight
|
||||
|
||||
function setSearchingText(text) {
|
||||
searchWidget.setSearchingText(text);
|
||||
searchWidget.focusFirstItem();
|
||||
}
|
||||
|
||||
Column {
|
||||
id: columnLayout
|
||||
visible: GlobalStates.overviewOpen
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
}
|
||||
spacing: -8
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
GlobalStates.overviewOpen = false;
|
||||
} else if (event.key === Qt.Key_Left) {
|
||||
if (!root.searchingText)
|
||||
Hyprland.dispatch("workspace r-1");
|
||||
} else if (event.key === Qt.Key_Right) {
|
||||
if (!root.searchingText)
|
||||
Hyprland.dispatch("workspace r+1");
|
||||
}
|
||||
}
|
||||
|
||||
SearchWidget {
|
||||
id: searchWidget
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Synchronizer on searchingText {
|
||||
property alias source: root.searchingText
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overviewLoader
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
|
||||
sourceComponent: OverviewWidget {
|
||||
panelWindow: root
|
||||
visible: (root.searchingText == "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleClipboard() {
|
||||
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
|
||||
GlobalStates.overviewOpen = false;
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < overviewVariants.instances.length; i++) {
|
||||
let panelWindow = overviewVariants.instances[i];
|
||||
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
|
||||
overviewScope.dontAutoCancelSearch = true;
|
||||
panelWindow.setSearchingText(Config.options.search.prefix.clipboard);
|
||||
GlobalStates.overviewOpen = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEmojis() {
|
||||
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
|
||||
GlobalStates.overviewOpen = false;
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < overviewVariants.instances.length; i++) {
|
||||
let panelWindow = overviewVariants.instances[i];
|
||||
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
|
||||
overviewScope.dontAutoCancelSearch = true;
|
||||
panelWindow.setSearchingText(Config.options.search.prefix.emojis);
|
||||
GlobalStates.overviewOpen = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "overview"
|
||||
|
||||
function toggle() {
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
function close() {
|
||||
GlobalStates.overviewOpen = false;
|
||||
}
|
||||
function open() {
|
||||
GlobalStates.overviewOpen = true;
|
||||
}
|
||||
function toggleReleaseInterrupt() {
|
||||
GlobalStates.superReleaseMightTrigger = false;
|
||||
}
|
||||
function clipboardToggle() {
|
||||
overviewScope.toggleClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "overviewToggle"
|
||||
description: "Toggles overview on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "overviewClose"
|
||||
description: "Closes overview"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.overviewOpen = false;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "overviewToggleRelease"
|
||||
description: "Toggles overview on release"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.superReleaseMightTrigger = true;
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (!GlobalStates.superReleaseMightTrigger) {
|
||||
GlobalStates.superReleaseMightTrigger = true;
|
||||
return;
|
||||
}
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "overviewToggleReleaseInterrupt"
|
||||
description: "Interrupts possibility of overview being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything."
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.superReleaseMightTrigger = false;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "overviewClipboardToggle"
|
||||
description: "Toggle clipboard query on overview widget"
|
||||
|
||||
onPressed: {
|
||||
overviewScope.toggleClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "overviewEmojiToggle"
|
||||
description: "Toggle emoji query on overview widget"
|
||||
|
||||
onPressed: {
|
||||
overviewScope.toggleEmojis();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property var panelWindow
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
|
||||
readonly property var toplevels: ToplevelManager.toplevels
|
||||
readonly property int workspacesShown: Config.options.overview.rows * Config.options.overview.columns
|
||||
readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
|
||||
property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name)
|
||||
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: Config.options.overview.scale
|
||||
property color activeBorderColor: Appearance.colors.colSecondary
|
||||
|
||||
property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ?
|
||||
((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) :
|
||||
((monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale)
|
||||
property real workspaceImplicitHeight: (monitorData?.transform % 2 === 1) ?
|
||||
((monitor.width - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) :
|
||||
((monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale)
|
||||
property real largeWorkspaceRadius: Appearance.rounding.large
|
||||
property real smallWorkspaceRadius: Appearance.rounding.verysmall
|
||||
|
||||
property real workspaceNumberMargin: 80
|
||||
property real workspaceNumberSize: 250 * monitor.scale
|
||||
property int workspaceZ: 0
|
||||
property int windowZ: 1
|
||||
property int windowDraggingZ: 99999
|
||||
property real workspaceSpacing: 5
|
||||
|
||||
property int draggingFromWorkspace: -1
|
||||
property int draggingTargetWorkspace: -1
|
||||
|
||||
implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
|
||||
implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2
|
||||
|
||||
property Component windowComponent: OverviewWindow {}
|
||||
property list<OverviewWindow> windowWidgets: []
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: overviewBackground
|
||||
}
|
||||
Rectangle { // Background
|
||||
id: overviewBackground
|
||||
property real padding: 10
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.sizes.elevationMargin
|
||||
|
||||
implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
|
||||
implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
|
||||
radius: root.largeWorkspaceRadius + padding
|
||||
color: Appearance.colors.colBackgroundSurfaceContainer
|
||||
|
||||
Column { // Workspaces
|
||||
id: workspaceColumnLayout
|
||||
|
||||
z: root.workspaceZ
|
||||
anchors.centerIn: parent
|
||||
spacing: workspaceSpacing
|
||||
|
||||
Repeater {
|
||||
model: Config.options.overview.rows
|
||||
delegate: Row {
|
||||
id: row
|
||||
required property int index
|
||||
spacing: workspaceSpacing
|
||||
|
||||
Repeater { // Workspace repeater
|
||||
model: Config.options.overview.columns
|
||||
Rectangle { // Workspace
|
||||
id: workspace
|
||||
required property int index
|
||||
property int colIndex: index
|
||||
property int workspaceValue: root.workspaceGroup * root.workspacesShown + row.index * Config.options.overview.columns + colIndex + 1
|
||||
property color defaultWorkspaceColor: ColorUtils.mix(Appearance.colors.colBackgroundSurfaceContainer, Appearance.colors.colSurfaceContainerHigh, 0.8)
|
||||
property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
|
||||
property color hoveredBorderColor: Appearance.colors.colLayer2Hover
|
||||
property bool hoveredWhileDragging: false
|
||||
|
||||
implicitWidth: root.workspaceImplicitWidth
|
||||
implicitHeight: root.workspaceImplicitHeight
|
||||
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
|
||||
property bool workspaceAtLeft: colIndex === 0
|
||||
property bool workspaceAtRight: colIndex === Config.options.overview.columns - 1
|
||||
property bool workspaceAtTop: row.index === 0
|
||||
property bool workspaceAtBottom: row.index === Config.options.overview.rows - 1
|
||||
topLeftRadius: (workspaceAtLeft && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
topRightRadius: (workspaceAtRight && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
bottomLeftRadius: (workspaceAtLeft && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
bottomRightRadius: (workspaceAtRight && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
border.width: 2
|
||||
border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: workspace.workspaceValue
|
||||
font {
|
||||
pixelSize: root.workspaceNumberSize * root.scale
|
||||
weight: Font.DemiBold
|
||||
family: Appearance.font.family.expressive
|
||||
}
|
||||
color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: workspaceArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressed: {
|
||||
if (root.draggingTargetWorkspace === -1) {
|
||||
GlobalStates.overviewOpen = false
|
||||
Hyprland.dispatch(`workspace ${workspace.workspaceValue}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onEntered: {
|
||||
root.draggingTargetWorkspace = workspace.workspaceValue
|
||||
if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return;
|
||||
hoveredWhileDragging = true
|
||||
}
|
||||
onExited: {
|
||||
hoveredWhileDragging = false
|
||||
if (root.draggingTargetWorkspace == workspace.workspaceValue) root.draggingTargetWorkspace = -1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { // Windows & focused workspace indicator
|
||||
id: windowSpace
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: workspaceColumnLayout.implicitWidth
|
||||
implicitHeight: workspaceColumnLayout.implicitHeight
|
||||
|
||||
Repeater { // Window repeater
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
// console.log(JSON.stringify(ToplevelManager.toplevels.values.map(t => t), null, 2))
|
||||
return [...ToplevelManager.toplevels.values.filter((toplevel) => {
|
||||
const address = `0x${toplevel.HyprlandToplevel?.address}`
|
||||
var win = windowByAddress[address]
|
||||
const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
|
||||
return inWorkspaceGroup;
|
||||
})].reverse()
|
||||
}
|
||||
}
|
||||
delegate: OverviewWindow {
|
||||
id: window
|
||||
required property var modelData
|
||||
property int monitorId: windowData?.monitor
|
||||
property var monitor: HyprlandData.monitors.find(m => m.id == monitorId)
|
||||
property var address: `0x${modelData.HyprlandToplevel.address}`
|
||||
toplevel: modelData
|
||||
monitorData: this.monitor
|
||||
scale: root.scale
|
||||
widgetMonitor: HyprlandData.monitors.find(m => m.id == root.monitor.id)
|
||||
windowData: windowByAddress[address]
|
||||
|
||||
property bool atInitPosition: (initX == x && initY == y)
|
||||
|
||||
// Offset on the canvas
|
||||
property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns
|
||||
property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns)
|
||||
xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
|
||||
yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
|
||||
property real xWithinWorkspaceWidget: Math.max((windowData?.at[0] - (monitor?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0)
|
||||
property real yWithinWorkspaceWidget: Math.max((windowData?.at[1] - (monitor?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0)
|
||||
|
||||
// Radius
|
||||
property real minRadius: Appearance.rounding.small
|
||||
property bool workspaceAtLeft: workspaceColIndex === 0
|
||||
property bool workspaceAtRight: workspaceColIndex === Config.options.overview.columns - 1
|
||||
property bool workspaceAtTop: workspaceRowIndex === 0
|
||||
property bool workspaceAtBottom: workspaceRowIndex === Config.options.overview.rows - 1
|
||||
property bool workspaceAtTopLeft: (workspaceAtLeft && workspaceAtTop)
|
||||
property bool workspaceAtTopRight: (workspaceAtRight && workspaceAtTop)
|
||||
property bool workspaceAtBottomLeft: (workspaceAtLeft && workspaceAtBottom)
|
||||
property bool workspaceAtBottomRight: (workspaceAtRight && workspaceAtBottom)
|
||||
property real distanceFromLeftEdge: xWithinWorkspaceWidget
|
||||
property real distanceFromRightEdge: root.workspaceImplicitWidth - (xWithinWorkspaceWidget + targetWindowWidth)
|
||||
property real distanceFromTopEdge: yWithinWorkspaceWidget
|
||||
property real distanceFromBottomEdge: root.workspaceImplicitHeight - (yWithinWorkspaceWidget + targetWindowHeight)
|
||||
property real distanceFromTopLeftCorner: Math.max(distanceFromLeftEdge, distanceFromTopEdge)
|
||||
property real distanceFromTopRightCorner: Math.max(distanceFromRightEdge, distanceFromTopEdge)
|
||||
property real distanceFromBottomLeftCorner: Math.max(distanceFromLeftEdge, distanceFromBottomEdge)
|
||||
property real distanceFromBottomRightCorner: Math.max(distanceFromRightEdge, distanceFromBottomEdge)
|
||||
topLeftRadius: Math.max((workspaceAtTopLeft ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromTopLeftCorner, minRadius)
|
||||
topRightRadius: Math.max((workspaceAtTopRight ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromTopRightCorner, minRadius)
|
||||
bottomLeftRadius: Math.max((workspaceAtBottomLeft ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromBottomLeftCorner, minRadius)
|
||||
bottomRightRadius: Math.max((workspaceAtBottomRight ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromBottomRightCorner, minRadius)
|
||||
|
||||
Timer {
|
||||
id: updateWindowPosition
|
||||
interval: Config.options.hacks.arbitraryRaceConditionDelay
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
window.x = Math.round(xWithinWorkspaceWidget + xOffset)
|
||||
window.y = Math.round(yWithinWorkspaceWidget + yOffset)
|
||||
}
|
||||
}
|
||||
|
||||
z: Drag.active ? root.windowDraggingZ : (root.windowZ + windowData?.floating)
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: hovered = true // For hover color change
|
||||
onExited: hovered = false // For hover color change
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
drag.target: parent
|
||||
onPressed: (mouse) => {
|
||||
root.draggingFromWorkspace = windowData?.workspace.id
|
||||
window.pressed = true
|
||||
window.Drag.active = true
|
||||
window.Drag.source = window
|
||||
window.Drag.hotSpot.x = mouse.x
|
||||
window.Drag.hotSpot.y = mouse.y
|
||||
// console.log(`[OverviewWindow] Dragging window ${windowData?.address} from position (${window.x}, ${window.y})`)
|
||||
}
|
||||
onReleased: {
|
||||
const targetWorkspace = root.draggingTargetWorkspace
|
||||
window.pressed = false
|
||||
window.Drag.active = false
|
||||
root.draggingFromWorkspace = -1
|
||||
if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
|
||||
Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`)
|
||||
updateWindowPosition.restart()
|
||||
}
|
||||
else {
|
||||
if (!window.windowData.floating) {
|
||||
updateWindowPosition.restart()
|
||||
return
|
||||
}
|
||||
const percentageX = Math.round((window.x - xOffset) / root.workspaceImplicitWidth * 100)
|
||||
const percentageY = Math.round((window.y - yOffset) / root.workspaceImplicitHeight * 100)
|
||||
Hyprland.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${window.windowData?.address}`)
|
||||
}
|
||||
}
|
||||
onClicked: (event) => {
|
||||
if (!windowData) return;
|
||||
|
||||
if (event.button === Qt.LeftButton) {
|
||||
GlobalStates.overviewOpen = false
|
||||
Hyprland.dispatch(`focuswindow address:${windowData.address}`)
|
||||
event.accepted = true
|
||||
} else if (event.button === Qt.MiddleButton) {
|
||||
Hyprland.dispatch(`closewindow address:${windowData.address}`)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
extraVisibleCondition: false
|
||||
alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active
|
||||
text: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Focused workspace indicator
|
||||
id: focusedWorkspaceIndicator
|
||||
property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown)
|
||||
property int rowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns)
|
||||
property int colIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns
|
||||
x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex
|
||||
y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex
|
||||
z: root.windowZ
|
||||
width: root.workspaceImplicitWidth
|
||||
height: root.workspaceImplicitHeight
|
||||
color: "transparent"
|
||||
property bool workspaceAtLeft: colIndex === 0
|
||||
property bool workspaceAtRight: colIndex === Config.options.overview.columns - 1
|
||||
property bool workspaceAtTop: rowIndex === 0
|
||||
property bool workspaceAtBottom: rowIndex === Config.options.overview.rows - 1
|
||||
topLeftRadius: (workspaceAtLeft && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
topRightRadius: (workspaceAtRight && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
bottomLeftRadius: (workspaceAtLeft && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
bottomRightRadius: (workspaceAtRight && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
|
||||
border.width: 2
|
||||
border.color: root.activeBorderColor
|
||||
Behavior on x {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on y {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on topLeftRadius {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on topRightRadius {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on bottomLeftRadius {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on bottomRightRadius {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
Item { // Window
|
||||
id: root
|
||||
property var toplevel
|
||||
property var windowData
|
||||
property var monitorData
|
||||
property var scale
|
||||
property bool restrictToWorkspace: true
|
||||
property real widthRatio: {
|
||||
const widgetWidth = widgetMonitor.transform & 1 ? widgetMonitor.height : widgetMonitor.width;
|
||||
const monitorWidth = monitorData.transform & 1 ? monitorData.height : monitorData.width;
|
||||
return (widgetWidth * monitorData.scale) / (monitorWidth * widgetMonitor.scale);
|
||||
}
|
||||
property real heightRatio: {
|
||||
const widgetHeight = widgetMonitor.transform & 1 ? widgetMonitor.width : widgetMonitor.height;
|
||||
const monitorHeight = monitorData.transform & 1 ? monitorData.width : monitorData.height;
|
||||
return (widgetHeight * monitorData.scale) / (monitorHeight * widgetMonitor.scale);
|
||||
}
|
||||
property real initX: {
|
||||
return Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * widthRatio * root.scale, 0) + xOffset;
|
||||
}
|
||||
|
||||
property real initY: {
|
||||
return Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * heightRatio * root.scale, 0) + yOffset;
|
||||
}
|
||||
property real xOffset: 0
|
||||
property real yOffset: 0
|
||||
property var widgetMonitor
|
||||
property int widgetMonitorId: widgetMonitor.id
|
||||
|
||||
property var targetWindowWidth: windowData?.size[0] * scale * widthRatio
|
||||
property var targetWindowHeight: windowData?.size[1] * scale * heightRatio
|
||||
property bool hovered: false
|
||||
property bool pressed: false
|
||||
|
||||
property bool centerIcons: Config.options.overview.centerIcons
|
||||
property real iconGapRatio: 0.06
|
||||
property real iconToWindowRatio: centerIcons ? 0.35 : 0.15
|
||||
property real xwaylandIndicatorToIconRatio: 0.35
|
||||
property real iconToWindowRatioCompact: 0.6
|
||||
property string iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing")
|
||||
property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
|
||||
|
||||
property bool indicateXWayland: windowData?.xwayland ?? false
|
||||
|
||||
x: initX
|
||||
y: initY
|
||||
width: targetWindowWidth
|
||||
height: targetWindowHeight
|
||||
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
|
||||
|
||||
property real topLeftRadius
|
||||
property real topRightRadius
|
||||
property real bottomLeftRadius
|
||||
property real bottomRightRadius
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: root.width
|
||||
height: root.height
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on y {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on width {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: windowPreview
|
||||
anchors.fill: parent
|
||||
captureSource: GlobalStates.overviewOpen ? root.toplevel : null
|
||||
live: true
|
||||
|
||||
// Color overlay for interactions
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
color: pressed ? ColorUtils.transparentize(Appearance.colors.colLayer2Active, 0.5) :
|
||||
hovered ? ColorUtils.transparentize(Appearance.colors.colLayer2Hover, 0.7) :
|
||||
ColorUtils.transparentize(Appearance.colors.colLayer2)
|
||||
border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.88)
|
||||
border.width : 1
|
||||
}
|
||||
|
||||
Image {
|
||||
id: windowIcon
|
||||
property real baseSize: Math.min(root.targetWindowWidth, root.targetWindowHeight)
|
||||
anchors {
|
||||
top: root.centerIcons ? undefined : parent.top
|
||||
left: root.centerIcons ? undefined : parent.left
|
||||
centerIn: root.centerIcons ? parent : undefined
|
||||
margins: baseSize * root.iconGapRatio
|
||||
}
|
||||
property var iconSize: {
|
||||
// console.log("-=-=-", root.toplevel.title, "-=-=-")
|
||||
// console.log("Target window size:", targetWindowWidth, targetWindowHeight)
|
||||
// console.log("Icon ratio:", root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
|
||||
// console.log("Scale:", root.monitorData.scale)
|
||||
// console.log("Final:", Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale)
|
||||
return baseSize * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio);
|
||||
}
|
||||
// mipmap: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: root.iconPath
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
sourceSize: Qt.size(iconSize, iconSize)
|
||||
|
||||
Behavior on width {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
spacing: 6
|
||||
property bool animateWidth: false
|
||||
property alias searchInput: searchInput
|
||||
property string searchingText
|
||||
|
||||
function forceFocus() {
|
||||
searchInput.forceActiveFocus();
|
||||
}
|
||||
|
||||
enum SearchPrefixType { Action, App, Clipboard, Emojis, Math, ShellCommand, WebSearch, DefaultSearch }
|
||||
|
||||
property var searchPrefixType: {
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.action)) return SearchBar.SearchPrefixType.Action;
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.app)) return SearchBar.SearchPrefixType.App;
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) return SearchBar.SearchPrefixType.Clipboard;
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) return SearchBar.SearchPrefixType.Emojis;
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.math)) return SearchBar.SearchPrefixType.Math;
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.shellCommand)) return SearchBar.SearchPrefixType.ShellCommand;
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.webSearch)) return SearchBar.SearchPrefixType.WebSearch;
|
||||
return SearchBar.SearchPrefixType.DefaultSearch;
|
||||
}
|
||||
|
||||
MaterialShapeWrappedMaterialSymbol {
|
||||
id: searchIcon
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
shape: switch(root.searchPrefixType) {
|
||||
case SearchBar.SearchPrefixType.Action: return MaterialShape.Shape.Pill;
|
||||
case SearchBar.SearchPrefixType.App: return MaterialShape.Shape.Clover4Leaf;
|
||||
case SearchBar.SearchPrefixType.Clipboard: return MaterialShape.Shape.Gem;
|
||||
case SearchBar.SearchPrefixType.Emojis: return MaterialShape.Shape.Sunny;
|
||||
case SearchBar.SearchPrefixType.Math: return MaterialShape.Shape.PuffyDiamond;
|
||||
case SearchBar.SearchPrefixType.ShellCommand: return MaterialShape.Shape.PixelCircle;
|
||||
case SearchBar.SearchPrefixType.WebSearch: return MaterialShape.Shape.SoftBurst;
|
||||
default: return MaterialShape.Shape.Cookie7Sided;
|
||||
}
|
||||
text: switch (root.searchPrefixType) {
|
||||
case SearchBar.SearchPrefixType.Action: return "settings_suggest";
|
||||
case SearchBar.SearchPrefixType.App: return "apps";
|
||||
case SearchBar.SearchPrefixType.Clipboard: return "content_paste_search";
|
||||
case SearchBar.SearchPrefixType.Emojis: return "add_reaction";
|
||||
case SearchBar.SearchPrefixType.Math: return "calculate";
|
||||
case SearchBar.SearchPrefixType.ShellCommand: return "terminal";
|
||||
case SearchBar.SearchPrefixType.WebSearch: return "travel_explore";
|
||||
case SearchBar.SearchPrefixType.DefaultSearch: return "search";
|
||||
default: return "search";
|
||||
}
|
||||
}
|
||||
ToolbarTextField { // Search box
|
||||
id: searchInput
|
||||
Layout.topMargin: 4
|
||||
Layout.bottomMargin: 4
|
||||
implicitHeight: 40
|
||||
focus: GlobalStates.overviewOpen
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
placeholderText: Translation.tr("Search, calculate or run")
|
||||
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
|
||||
|
||||
Behavior on implicitWidth {
|
||||
id: searchWidthBehavior
|
||||
enabled: root.animateWidth
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: root.searchingText = text
|
||||
|
||||
onAccepted: {
|
||||
if (appResults.count > 0) {
|
||||
// Get the first visible delegate and trigger its click
|
||||
let firstItem = appResults.itemAtIndex(0);
|
||||
if (firstItem && firstItem.clicked) {
|
||||
firstItem.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconToolbarButton {
|
||||
Layout.topMargin: 4
|
||||
Layout.bottomMargin: 4
|
||||
onClicked: {
|
||||
GlobalStates.overviewOpen = false;
|
||||
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "search"]);
|
||||
}
|
||||
text: "image_search"
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Google Lens")
|
||||
}
|
||||
}
|
||||
|
||||
IconToolbarButton {
|
||||
id: songRecButton
|
||||
Layout.topMargin: 4
|
||||
Layout.bottomMargin: 4
|
||||
Layout.rightMargin: 4
|
||||
toggled: SongRec.running
|
||||
onClicked: SongRec.toggleRunning()
|
||||
text: "music_cast"
|
||||
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Recognize music")
|
||||
}
|
||||
|
||||
colText: toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSurfaceVariant
|
||||
background: MaterialShape {
|
||||
RotationAnimation on rotation {
|
||||
running: songRecButton.toggled
|
||||
duration: 12000
|
||||
easing.type: Easing.Linear
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
}
|
||||
shape: {
|
||||
if (songRecButton.down) {
|
||||
return songRecButton.toggled ? MaterialShape.Shape.Circle : MaterialShape.Shape.Square
|
||||
} else {
|
||||
return songRecButton.toggled ? MaterialShape.Shape.SoftBurst : MaterialShape.Shape.Circle
|
||||
}
|
||||
}
|
||||
color: {
|
||||
if (songRecButton.toggled) {
|
||||
return songRecButton.hovered ? Appearance.colors.colPrimaryHover : Appearance.colors.colPrimary
|
||||
} else {
|
||||
return songRecButton.hovered ? Appearance.colors.colSurfaceContainerHigh : ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHigh)
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// pragma NativeMethodBehavior: AcceptThisObject
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property var entry
|
||||
property string query
|
||||
property bool entryShown: entry?.shown ?? true
|
||||
property string itemType: entry?.type ?? Translation.tr("App")
|
||||
property string itemName: entry?.name ?? ""
|
||||
property string itemIcon: entry?.icon ?? ""
|
||||
property var itemExecute: entry?.execute
|
||||
property string fontType: entry?.fontType ?? "main"
|
||||
property string itemClickActionName: entry?.clickActionName ?? "Open"
|
||||
property string bigText: entry?.bigText ?? ""
|
||||
property string materialSymbol: entry?.materialSymbol ?? ""
|
||||
property string cliphistRawString: entry?.cliphistRawString ?? ""
|
||||
property bool blurImage: entry?.blurImage ?? false
|
||||
property string blurImageText: entry?.blurImageText ?? "Image hidden"
|
||||
|
||||
visible: root.entryShown
|
||||
property int horizontalMargin: 10
|
||||
property int buttonHorizontalPadding: 10
|
||||
property int buttonVerticalPadding: 6
|
||||
property bool keyboardDown: false
|
||||
|
||||
implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2
|
||||
implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2
|
||||
buttonRadius: Appearance.rounding.normal
|
||||
colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colPrimaryContainerActive :
|
||||
((root.hovered || root.focus) ? Appearance.colors.colPrimaryContainer :
|
||||
ColorUtils.transparentize(Appearance.colors.colPrimaryContainer, 1))
|
||||
colBackgroundHover: Appearance.colors.colPrimaryContainer
|
||||
colRipple: Appearance.colors.colPrimaryContainerActive
|
||||
|
||||
property string highlightPrefix: `<u><font color="${Appearance.colors.colPrimary}">`
|
||||
property string highlightSuffix: `</font></u>`
|
||||
function highlightContent(content, query) {
|
||||
if (!query || query.length === 0 || content == query || fontType === "monospace")
|
||||
return StringUtils.escapeHtml(content);
|
||||
|
||||
let contentLower = content.toLowerCase();
|
||||
let queryLower = query.toLowerCase();
|
||||
|
||||
let result = "";
|
||||
let lastIndex = 0;
|
||||
let qIndex = 0;
|
||||
|
||||
for (let i = 0; i < content.length && qIndex < query.length; i++) {
|
||||
if (contentLower[i] === queryLower[qIndex]) {
|
||||
// Add non-highlighted part (escaped)
|
||||
if (i > lastIndex)
|
||||
result += StringUtils.escapeHtml(content.slice(lastIndex, i));
|
||||
// Add highlighted character (escaped)
|
||||
result += root.highlightPrefix + StringUtils.escapeHtml(content[i]) + root.highlightSuffix;
|
||||
lastIndex = i + 1;
|
||||
qIndex++;
|
||||
}
|
||||
}
|
||||
// Add the rest of the string (escaped)
|
||||
if (lastIndex < content.length)
|
||||
result += StringUtils.escapeHtml(content.slice(lastIndex));
|
||||
|
||||
return result;
|
||||
}
|
||||
property string displayContent: highlightContent(root.itemName, root.query)
|
||||
|
||||
property list<string> urls: {
|
||||
if (!root.itemName) return [];
|
||||
// Regular expression to match URLs
|
||||
const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
|
||||
const matches = root.itemName?.match(urlRegex)
|
||||
?.filter(url => !url.includes("…")) // Elided = invalid
|
||||
return matches ? matches : [];
|
||||
}
|
||||
|
||||
PointingHandInteraction {}
|
||||
|
||||
background {
|
||||
anchors.fill: root
|
||||
anchors.leftMargin: root.horizontalMargin
|
||||
anchors.rightMargin: root.horizontalMargin
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
GlobalStates.overviewOpen = false
|
||||
root.itemExecute()
|
||||
}
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) {
|
||||
const deleteAction = root.entry.actions.find(action => action.name == "Delete");
|
||||
|
||||
if (deleteAction) {
|
||||
deleteAction.execute()
|
||||
}
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.keyboardDown = true
|
||||
root.clicked()
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
Keys.onReleased: (event) => {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.keyboardDown = false
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
spacing: iconLoader.sourceComponent === null ? 0 : 10
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding
|
||||
anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding
|
||||
|
||||
// Icon
|
||||
Loader {
|
||||
id: iconLoader
|
||||
active: true
|
||||
sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent :
|
||||
root.bigText ? bigTextComponent :
|
||||
root.itemIcon !== "" ? iconImageComponent :
|
||||
null
|
||||
}
|
||||
|
||||
Component {
|
||||
id: iconImageComponent
|
||||
IconImage {
|
||||
source: Quickshell.iconPath(root.itemIcon, "image-missing")
|
||||
width: 35
|
||||
height: 35
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: materialSymbolComponent
|
||||
MaterialSymbol {
|
||||
text: root.materialSymbol
|
||||
iconSize: 30
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: bigTextComponent
|
||||
StyledText {
|
||||
text: root.bigText
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
// Main text
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 0
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: Appearance.colors.colSubtext
|
||||
visible: root.itemType && root.itemType != Translation.tr("App")
|
||||
text: root.itemType
|
||||
}
|
||||
RowLayout {
|
||||
Loader { // Checkmark for copied clipboard entry
|
||||
visible: itemName == Quickshell.clipboardText && root.cliphistRawString
|
||||
active: itemName == Quickshell.clipboardText && root.cliphistRawString
|
||||
sourceComponent: Rectangle {
|
||||
implicitWidth: activeText.implicitHeight
|
||||
implicitHeight: activeText.implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
color: Appearance.colors.colPrimary
|
||||
MaterialSymbol {
|
||||
id: activeText
|
||||
anchors.centerIn: parent
|
||||
text: "check"
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3onPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
Repeater { // Favicons for links
|
||||
model: root.query == root.itemName ? [] : root.urls
|
||||
Favicon {
|
||||
required property var modelData
|
||||
size: parent.height
|
||||
url: modelData
|
||||
}
|
||||
}
|
||||
StyledText { // Item name/content
|
||||
Layout.fillWidth: true
|
||||
id: nameText
|
||||
textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
font.family: Appearance.font.family[root.fontType]
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
elide: Text.ElideRight
|
||||
text: `${root.displayContent}`
|
||||
}
|
||||
}
|
||||
Loader { // Clipboard image preview
|
||||
active: root.cliphistRawString && Cliphist.entryIsImage(root.cliphistRawString)
|
||||
sourceComponent: CliphistImage {
|
||||
Layout.fillWidth: true
|
||||
entry: root.cliphistRawString
|
||||
maxWidth: contentColumn.width
|
||||
maxHeight: 140
|
||||
blur: root.blurImage
|
||||
blurText: root.blurImageText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action text
|
||||
StyledText {
|
||||
Layout.fillWidth: false
|
||||
visible: (root.hovered || root.focus)
|
||||
id: clickAction
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: root.itemClickActionName
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: root.buttonVerticalPadding
|
||||
Layout.bottomMargin: -root.buttonVerticalPadding // Why is this necessary? Good question.
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: (root.entry.actions ?? []).slice(0, 4)
|
||||
delegate: RippleButton {
|
||||
id: actionButton
|
||||
required property var modelData
|
||||
property string iconName: modelData.icon ?? ""
|
||||
property string materialIconName: modelData.materialIcon ?? ""
|
||||
implicitHeight: 34
|
||||
implicitWidth: 34
|
||||
|
||||
colBackgroundHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRipple: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: Item {
|
||||
id: actionContentItem
|
||||
anchors.centerIn: parent
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
active: !(actionButton.iconName !== "") || actionButton.materialIconName
|
||||
sourceComponent: MaterialSymbol {
|
||||
text: actionButton.materialIconName || "video_settings"
|
||||
font.pixelSize: Appearance.font.pixelSize.hugeass
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
active: actionButton.materialIconName.length == 0 && actionButton.iconName && actionButton.iconName !== ""
|
||||
sourceComponent: IconImage {
|
||||
source: Quickshell.iconPath(actionButton.iconName)
|
||||
implicitSize: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: modelData.execute()
|
||||
|
||||
StyledToolTip {
|
||||
text: modelData.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import Qt.labs.synchronizer
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Item { // Wrapper
|
||||
id: root
|
||||
readonly property string xdgConfigHome: Directories.config
|
||||
property string searchingText: ""
|
||||
property bool showResults: searchingText != ""
|
||||
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
|
||||
implicitHeight: searchBar.implicitHeight + searchBar.verticalPadding * 2 + Appearance.sizes.elevationMargin * 2
|
||||
|
||||
property string mathResult: ""
|
||||
property bool clipboardWorkSafetyActive: {
|
||||
const enabled = Config.options.workSafety.enable.clipboard;
|
||||
const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords))
|
||||
return enabled && sensitiveNetwork;
|
||||
}
|
||||
|
||||
property var searchActions: [
|
||||
{
|
||||
action: "accentcolor",
|
||||
execute: args => {
|
||||
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--noswitch", "--color", ...(args != '' ? [`${args}`] : [])]);
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "dark",
|
||||
execute: () => {
|
||||
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]);
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "konachanwallpaper",
|
||||
execute: () => {
|
||||
Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random/random_konachan_wall.sh")]);
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "light",
|
||||
execute: () => {
|
||||
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]);
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "superpaste",
|
||||
execute: args => {
|
||||
if (!/^(\d+)/.test(args.trim())) { // Invalid if doesn't start with numbers
|
||||
Quickshell.execDetached([
|
||||
"notify-send",
|
||||
Translation.tr("Superpaste"),
|
||||
Translation.tr("Usage: <tt>%1superpaste NUM_OF_ENTRIES[i]</tt>\nSupply <tt>i</tt> when you want images\nExamples:\n<tt>%1superpaste 4i</tt> for the last 4 images\n<tt>%1superpaste 7</tt> for the last 7 entries").arg(Config.options.search.prefix.action),
|
||||
"-a", "Shell"
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const syntaxMatch = /^(?:(\d+)(i)?)/.exec(args.trim());
|
||||
const count = syntaxMatch[1] ? parseInt(syntaxMatch[1]) : 1;
|
||||
const isImage = !!syntaxMatch[2];
|
||||
Cliphist.superpaste(count, isImage);
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "todo",
|
||||
execute: args => {
|
||||
Todo.addTask(args);
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "wallpaper",
|
||||
execute: () => {
|
||||
GlobalStates.wallpaperSelectorOpen = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "wipeclipboard",
|
||||
execute: () => {
|
||||
Cliphist.wipe();
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
function focusFirstItem() {
|
||||
appResults.currentIndex = 0;
|
||||
}
|
||||
|
||||
function focusSearchInput() {
|
||||
searchBar.forceFocus();
|
||||
}
|
||||
|
||||
function disableExpandAnimation() {
|
||||
searchBar.animateWidth = false;
|
||||
}
|
||||
|
||||
function cancelSearch() {
|
||||
searchBar.searchInput.selectAll();
|
||||
root.searchingText = "";
|
||||
searchBar.animateWidth = true;
|
||||
}
|
||||
|
||||
function setSearchingText(text) {
|
||||
searchBar.searchInput.text = text;
|
||||
root.searchingText = text;
|
||||
}
|
||||
|
||||
function containsUnsafeLink(entry) {
|
||||
if (entry == undefined) return false;
|
||||
const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords;
|
||||
return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords);
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: nonAppResultsTimer
|
||||
interval: Config.options.search.nonAppResultDelay
|
||||
onTriggered: {
|
||||
let expr = root.searchingText;
|
||||
if (expr.startsWith(Config.options.search.prefix.math)) {
|
||||
expr = expr.slice(Config.options.search.prefix.math.length);
|
||||
}
|
||||
mathProcess.calculateExpression(expr);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: mathProcess
|
||||
property list<string> baseCommand: ["qalc", "-t"]
|
||||
function calculateExpression(expression) {
|
||||
mathProcess.running = false;
|
||||
mathProcess.command = baseCommand.concat(expression);
|
||||
mathProcess.running = true;
|
||||
}
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.mathResult = data;
|
||||
root.focusFirstItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
// Prevent Esc and Backspace from registering
|
||||
if (event.key === Qt.Key_Escape)
|
||||
return;
|
||||
|
||||
// Handle Backspace: focus and delete character if not focused
|
||||
if (event.key === Qt.Key_Backspace) {
|
||||
if (!searchBar.searchInput.activeFocus) {
|
||||
root.focusSearchInput();
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
// Delete word before cursor
|
||||
let text = searchBar.searchInput.text;
|
||||
let pos = searchBar.searchInput.cursorPosition;
|
||||
if (pos > 0) {
|
||||
// Find the start of the previous word
|
||||
let left = text.slice(0, pos);
|
||||
let match = left.match(/(\s*\S+)\s*$/);
|
||||
let deleteLen = match ? match[0].length : 1;
|
||||
searchBar.searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
|
||||
searchBar.searchInput.cursorPosition = pos - deleteLen;
|
||||
}
|
||||
} else {
|
||||
// Delete character before cursor if any
|
||||
if (searchBar.searchInput.cursorPosition > 0) {
|
||||
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition - 1) + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
|
||||
searchBar.searchInput.cursorPosition -= 1;
|
||||
}
|
||||
}
|
||||
// Always move cursor to end after programmatic edit
|
||||
searchBar.searchInput.cursorPosition = searchBar.searchInput.text.length;
|
||||
event.accepted = true;
|
||||
}
|
||||
// If already focused, let TextField handle it
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle visible printable characters (ignore control chars, arrows, etc.)
|
||||
if (event.text && event.text.length === 1 && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && event.key !== Qt.Key_Delete && event.text.charCodeAt(0) >= 0x20) // ignore control chars like Backspace, Tab, etc.
|
||||
{
|
||||
if (!searchBar.searchInput.activeFocus) {
|
||||
root.focusSearchInput();
|
||||
// Insert the character at the cursor position
|
||||
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition) + event.text + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
|
||||
searchBar.searchInput.cursorPosition += 1;
|
||||
event.accepted = true;
|
||||
root.focusFirstItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: searchWidgetContent
|
||||
}
|
||||
Rectangle { // Background
|
||||
id: searchWidgetContent
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: Appearance.sizes.elevationMargin
|
||||
}
|
||||
clip: true
|
||||
implicitWidth: columnLayout.implicitWidth
|
||||
implicitHeight: columnLayout.implicitHeight
|
||||
radius: searchBar.height / 2 + searchBar.verticalPadding
|
||||
color: Appearance.colors.colBackgroundSurfaceContainer
|
||||
|
||||
Behavior on implicitHeight {
|
||||
id: searchHeightBehavior
|
||||
enabled: GlobalStates.overviewOpen && root.showResults
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
// clip: true
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: searchWidgetContent.width
|
||||
height: searchWidgetContent.width
|
||||
radius: searchWidgetContent.radius
|
||||
}
|
||||
}
|
||||
|
||||
SearchBar {
|
||||
id: searchBar
|
||||
property real verticalPadding: 4
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 4
|
||||
Layout.topMargin: verticalPadding
|
||||
Layout.bottomMargin: verticalPadding
|
||||
Synchronizer on searchingText {
|
||||
property alias source: root.searchingText
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// Separator
|
||||
visible: root.showResults
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Appearance.colors.colOutlineVariant
|
||||
}
|
||||
|
||||
ListView { // App results
|
||||
id: appResults
|
||||
visible: root.showResults
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.min(600, appResults.contentHeight + topMargin + bottomMargin)
|
||||
clip: true
|
||||
topMargin: 10
|
||||
bottomMargin: 10
|
||||
spacing: 2
|
||||
KeyNavigation.up: searchBar
|
||||
highlightMoveDuration: 100
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus)
|
||||
appResults.currentIndex = 1;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onSearchingTextChanged() {
|
||||
if (appResults.count > 0)
|
||||
appResults.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
id: model
|
||||
objectProp: "key"
|
||||
values: {
|
||||
// Search results are handled here
|
||||
////////////////// Skip? //////////////////
|
||||
if (root.searchingText == "")
|
||||
return [];
|
||||
|
||||
///////////// Special cases ///////////////
|
||||
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) {
|
||||
// Clipboard
|
||||
const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.clipboard);
|
||||
return Cliphist.fuzzyQuery(searchString).map((entry, index, array) => {
|
||||
const mightBlurImage = Cliphist.entryIsImage(entry) && root.clipboardWorkSafetyActive;
|
||||
let shouldBlurImage = mightBlurImage;
|
||||
if (mightBlurImage) {
|
||||
shouldBlurImage = shouldBlurImage && (containsUnsafeLink(array[index - 1]) || containsUnsafeLink(array[index + 1]));
|
||||
}
|
||||
const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`
|
||||
return {
|
||||
key: type,
|
||||
cliphistRawString: entry,
|
||||
name: StringUtils.cleanCliphistEntry(entry),
|
||||
clickActionName: "",
|
||||
type: type,
|
||||
execute: () => {
|
||||
Cliphist.copy(entry)
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
name: "Copy",
|
||||
materialIcon: "content_copy",
|
||||
execute: () => {
|
||||
Cliphist.copy(entry);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
materialIcon: "delete",
|
||||
execute: () => {
|
||||
Cliphist.deleteEntry(entry);
|
||||
}
|
||||
}
|
||||
],
|
||||
blurImage: shouldBlurImage,
|
||||
blurImageText: Translation.tr("Work safety")
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
else if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) {
|
||||
// Clipboard
|
||||
const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.emojis);
|
||||
return Emojis.fuzzyQuery(searchString).map(entry => {
|
||||
const emoji = entry.match(/^\s*(\S+)/)?.[1] || ""
|
||||
return {
|
||||
key: emoji,
|
||||
cliphistRawString: entry,
|
||||
bigText: emoji,
|
||||
name: entry.replace(/^\s*\S+\s+/, ""),
|
||||
clickActionName: "",
|
||||
type: "Emoji",
|
||||
execute: () => {
|
||||
Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1];
|
||||
}
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
////////////////// Init ///////////////////
|
||||
nonAppResultsTimer.restart();
|
||||
const mathResultObject = {
|
||||
key: `Math result: ${root.mathResult}`,
|
||||
name: root.mathResult,
|
||||
clickActionName: Translation.tr("Copy"),
|
||||
type: Translation.tr("Math result"),
|
||||
fontType: "monospace",
|
||||
materialSymbol: 'calculate',
|
||||
execute: () => {
|
||||
Quickshell.clipboardText = root.mathResult;
|
||||
}
|
||||
};
|
||||
const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.app)).map(entry => {
|
||||
entry.clickActionName = Translation.tr("Launch");
|
||||
entry.type = Translation.tr("App");
|
||||
entry.key = entry.execute
|
||||
return entry;
|
||||
})
|
||||
const commandResultObject = {
|
||||
key: `cmd ${root.searchingText}`,
|
||||
name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.shellCommand).replace("file://", ""),
|
||||
clickActionName: Translation.tr("Run"),
|
||||
type: Translation.tr("Run command"),
|
||||
fontType: "monospace",
|
||||
materialSymbol: 'terminal',
|
||||
execute: () => {
|
||||
let cleanedCommand = root.searchingText.replace("file://", "");
|
||||
cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand);
|
||||
if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) {
|
||||
cleanedCommand = cleanedCommand.slice(Config.options.search.prefix.shellCommand.length);
|
||||
}
|
||||
Quickshell.execDetached(["bash", "-c", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]);
|
||||
}
|
||||
};
|
||||
const webSearchResultObject = {
|
||||
key: `website ${root.searchingText}`,
|
||||
name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch),
|
||||
clickActionName: Translation.tr("Search"),
|
||||
type: Translation.tr("Search the web"),
|
||||
materialSymbol: 'travel_explore',
|
||||
execute: () => {
|
||||
let query = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch);
|
||||
let url = Config.options.search.engineBaseUrl + query;
|
||||
for (let site of Config.options.search.excludedSites) {
|
||||
url += ` -site:${site}`;
|
||||
}
|
||||
Qt.openUrlExternally(url);
|
||||
}
|
||||
}
|
||||
const launcherActionObjects = root.searchActions.map(action => {
|
||||
const actionString = `${Config.options.search.prefix.action}${action.action}`;
|
||||
if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) {
|
||||
return {
|
||||
key: `Action ${actionString}`,
|
||||
name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString,
|
||||
clickActionName: Translation.tr("Run"),
|
||||
type: Translation.tr("Action"),
|
||||
materialSymbol: 'settings_suggest',
|
||||
execute: () => {
|
||||
action.execute(root.searchingText.split(" ").slice(1).join(" "));
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
//////// Prioritized by prefix /////////
|
||||
let result = [];
|
||||
const startsWithNumber = /^\d/.test(root.searchingText);
|
||||
const startsWithMathPrefix = root.searchingText.startsWith(Config.options.search.prefix.math);
|
||||
const startsWithShellCommandPrefix = root.searchingText.startsWith(Config.options.search.prefix.shellCommand);
|
||||
const startsWithWebSearchPrefix = root.searchingText.startsWith(Config.options.search.prefix.webSearch);
|
||||
if (startsWithNumber || startsWithMathPrefix) {
|
||||
result.push(mathResultObject);
|
||||
} else if (startsWithShellCommandPrefix) {
|
||||
result.push(commandResultObject);
|
||||
} else if (startsWithWebSearchPrefix) {
|
||||
result.push(webSearchResultObject);
|
||||
}
|
||||
|
||||
//////////////// Apps //////////////////
|
||||
result = result.concat(appResultObjects);
|
||||
|
||||
////////// Launcher actions ////////////
|
||||
result = result.concat(launcherActionObjects);
|
||||
|
||||
/// Math result, command, web search ///
|
||||
if (Config.options.search.prefix.showDefaultActionsWithoutPrefix) {
|
||||
if (!startsWithShellCommandPrefix) result.push(commandResultObject);
|
||||
if (!startsWithNumber && !startsWithMathPrefix) result.push(mathResultObject);
|
||||
if (!startsWithWebSearchPrefix) result.push(webSearchResultObject);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
delegate: SearchItem {
|
||||
// The selectable item for each search result
|
||||
required property var modelData
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
entry: modelData
|
||||
query: StringUtils.cleanOnePrefix(root.searchingText, [
|
||||
Config.options.search.prefix.action,
|
||||
Config.options.search.prefix.app,
|
||||
Config.options.search.prefix.clipboard,
|
||||
Config.options.search.prefix.emojis,
|
||||
Config.options.search.prefix.math,
|
||||
Config.options.search.prefix.shellCommand,
|
||||
Config.options.search.prefix.webSearch
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Loader {
|
||||
active: PolkitService.active
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: PanelWindow {
|
||||
id: panelWindow
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "quickshell:polkit"
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
PolkitContent {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
|
||||
|
||||
Keys.onPressed: event => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
PolkitService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
PolkitService.submit(inputField.text);
|
||||
}
|
||||
Connections {
|
||||
target: PolkitService
|
||||
function onInteractionAvailableChanged() {
|
||||
if (!PolkitService.interactionAvailable) return;
|
||||
inputField.text = "";
|
||||
inputField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
color: Appearance.colors.colScrim
|
||||
opacity: 0
|
||||
Component.onCompleted: {
|
||||
opacity = 1
|
||||
}
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialog {
|
||||
anchors.centerIn: parent
|
||||
backgroundWidth: 450
|
||||
show: false
|
||||
Component.onCompleted: {
|
||||
show = true
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
iconSize: 26
|
||||
text: "security"
|
||||
color: Appearance.colors.colSecondary
|
||||
}
|
||||
|
||||
WindowDialogTitle {
|
||||
id: titleText
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Translation.tr("Authentication")
|
||||
}
|
||||
|
||||
WindowDialogParagraph {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: {
|
||||
if (!PolkitService.flow) return;
|
||||
return PolkitService.flow.message.endsWith(".")
|
||||
? PolkitService.flow.message.slice(0, -1)
|
||||
: PolkitService.flow.message
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTextField {
|
||||
id: inputField
|
||||
Layout.fillWidth: true
|
||||
focus: true
|
||||
enabled: PolkitService.interactionAvailable
|
||||
placeholderText: {
|
||||
const inputPrompt = PolkitService.flow?.inputPrompt.trim() ?? "";
|
||||
const cleanedInputPrompt = inputPrompt.endsWith(":") ? inputPrompt.slice(0, -1) : inputPrompt;
|
||||
return cleanedInputPrompt || (root.usePasswordChars ? Translation.tr("Password") : Translation.tr("Input"))
|
||||
}
|
||||
echoMode: root.usePasswordChars ? TextInput.Password : TextInput.Normal
|
||||
onAccepted: root.submit();
|
||||
|
||||
Keys.onPressed: event => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
PolkitService.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialogButtonRow {
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Cancel")
|
||||
onClicked: PolkitService.cancel();
|
||||
}
|
||||
DialogButton {
|
||||
enabled: PolkitService.interactionAvailable
|
||||
buttonText: Translation.tr("OK")
|
||||
onClicked: root.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property color color
|
||||
required property color overlayColor
|
||||
required property list<point> points
|
||||
property int strokeWidth: Config.options.regionSelector.circle.strokeWidth
|
||||
|
||||
function updatePoints() {
|
||||
if (!root.dragging) return;
|
||||
root.points.push({ x: root.mouseX, y: root.mouseY });
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: darkenOverlay
|
||||
z: 1
|
||||
anchors.fill: parent
|
||||
color: root.overlayColor
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: shape
|
||||
z: 2
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: shapePath
|
||||
strokeWidth: root.strokeWidth
|
||||
pathHints: ShapePath.PathLinear
|
||||
fillColor: "transparent"
|
||||
strokeColor: root.color
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
|
||||
PathPolyline {
|
||||
path: root.points
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
// Options toolbar
|
||||
Toolbar {
|
||||
id: root
|
||||
|
||||
// Use a synchronizer on these
|
||||
property var action
|
||||
property var selectionMode
|
||||
// Signals
|
||||
signal dismiss()
|
||||
|
||||
MaterialShape {
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: 2
|
||||
Layout.rightMargin: 2
|
||||
implicitSize: 36 // Intentionally smaller because this one is brighter than others
|
||||
shape: switch (root.action) {
|
||||
case RegionSelection.SnipAction.Copy:
|
||||
case RegionSelection.SnipAction.Edit:
|
||||
return MaterialShape.Shape.Cookie4Sided;
|
||||
case RegionSelection.SnipAction.Search:
|
||||
return MaterialShape.Shape.Pentagon;
|
||||
case RegionSelection.SnipAction.CharRecognition:
|
||||
return MaterialShape.Shape.Sunny;
|
||||
case RegionSelection.SnipAction.Record:
|
||||
case RegionSelection.SnipAction.RecordWithSound:
|
||||
return MaterialShape.Shape.Gem;
|
||||
default:
|
||||
return MaterialShape.Shape.Cookie12Sided;
|
||||
}
|
||||
color: Appearance.colors.colPrimary
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: 22
|
||||
color: Appearance.colors.colOnPrimary
|
||||
animateChange: true
|
||||
text: switch (root.action) {
|
||||
case RegionSelection.SnipAction.Copy:
|
||||
case RegionSelection.SnipAction.Edit:
|
||||
return "content_cut";
|
||||
case RegionSelection.SnipAction.Search:
|
||||
return "image_search";
|
||||
case RegionSelection.SnipAction.CharRecognition:
|
||||
return "document_scanner";
|
||||
case RegionSelection.SnipAction.Record:
|
||||
case RegionSelection.SnipAction.RecordWithSound:
|
||||
return "videocam";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarTabBar {
|
||||
id: tabBar
|
||||
tabButtonList: [
|
||||
{"icon": "activity_zone", "name": Translation.tr("Rect")},
|
||||
{"icon": "gesture", "name": Translation.tr("Circle")}
|
||||
]
|
||||
currentIndex: root.selectionMode === RegionSelection.SelectionMode.RectCorners ? 0 : 1
|
||||
onCurrentIndexChanged: {
|
||||
root.selectionMode = currentIndex === 0 ? RegionSelection.SelectionMode.RectCorners : RegionSelection.SelectionMode.Circle;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property real regionX
|
||||
required property real regionY
|
||||
required property real regionWidth
|
||||
required property real regionHeight
|
||||
required property real mouseX
|
||||
required property real mouseY
|
||||
required property color color
|
||||
required property color overlayColor
|
||||
property bool showAimLines: Config.options.regionSelector.rect.showAimLines
|
||||
|
||||
// Overlay to darken screen
|
||||
// Base dark overlay around region
|
||||
Rectangle {
|
||||
id: darkenOverlay
|
||||
z: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
leftMargin: root.regionX - darkenOverlay.border.width
|
||||
topMargin: root.regionY - darkenOverlay.border.width
|
||||
}
|
||||
width: root.regionWidth + darkenOverlay.border.width * 2
|
||||
height: root.regionHeight + darkenOverlay.border.width * 2
|
||||
color: "transparent"
|
||||
border.color: root.overlayColor
|
||||
border.width: Math.max(root.width, root.height)
|
||||
}
|
||||
|
||||
// Selection border
|
||||
Rectangle {
|
||||
id: selectionBorder
|
||||
z: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
leftMargin: root.regionX
|
||||
topMargin: root.regionY
|
||||
}
|
||||
width: root.regionWidth
|
||||
height: root.regionHeight
|
||||
color: "transparent"
|
||||
border.color: root.color
|
||||
border.width: 2
|
||||
// radius: root.standardRounding
|
||||
radius: 0 // TODO: figure out how to make the overlay thing work with rounding
|
||||
}
|
||||
|
||||
StyledText {
|
||||
z: 2
|
||||
anchors {
|
||||
top: selectionBorder.bottom
|
||||
right: selectionBorder.right
|
||||
margins: 8
|
||||
}
|
||||
color: root.color
|
||||
text: `${Math.round(root.regionWidth)} x ${Math.round(root.regionHeight)}`
|
||||
}
|
||||
|
||||
// Coord lines
|
||||
Rectangle { // Vertical
|
||||
visible: root.showAimLines
|
||||
opacity: 0.2
|
||||
z: 2
|
||||
x: root.mouseX
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: 1
|
||||
color: root.color
|
||||
}
|
||||
Rectangle { // Horizontal
|
||||
visible: root.showAimLines
|
||||
opacity: 0.2
|
||||
z: 2
|
||||
y: root.mouseY
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: 1
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user