publish: the public projection begins here
This is a projection, not a development branch. The tree above was constructed from the internal source named below under a manifest that decides which paths may leave, then scanned as a whole tree rather than as a series of patches, and only then published. Public history starts here because the history before it was not admissible, and neither was the tree. What used to stand in this repository included a rescue copy of another machine, a directory of phone handoffs, deployment wired to one house, and a submodule pointing at a forge no stranger can reach. None of that was ever the product. It stays in the private forge, which is allowed to hold the whole working organism, and this is what was deliberately sent out instead. Three mechanisms produced this tree, in decreasing order of trust. A top-level path the manifest does not name never arrives at all, which is the one that catches directories nobody has thought of yet. Named internal files inside admitted roots are dropped. A short, reviewed table replaces deployment defaults that a public build must not carry -- an endpoint aimed at one LAN, a VPN profile belonging to one phone, packaging built from one checkout path. Everything after this commit is an ordinary publication with the same three trailers, so a force push stops being routine and starts meaning that something deliberate happened. The trailers bind the projection to its source without pretending the public SHA is the private one: same lineage, different tree, and the record says so. Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047 Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27 Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
commit
8f42fc953d
1476 changed files with 238455 additions and 0 deletions
310
surfaces/quickshell/ii-base/modules/ii/background/Background.qml
Normal file
310
surfaces/quickshell/ii-base/modules/ii/background/Background.qml
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
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
|
||||
import qs.modules.ii.background.widgets.visualizer
|
||||
import qs.modules.ii.background.widgets.stats
|
||||
import qs.modules.ii.background.widgets.resources
|
||||
|
||||
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
|
||||
property int workspaceChunkSize: Config?.options.bar.workspaces.shown ?? 10
|
||||
property int totalWorkspaces: Math.ceil(lastWorkspaceId / workspaceChunkSize) * workspaceChunkSize
|
||||
// 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;
|
||||
}
|
||||
readonly property real parallaxRation: Config.options.background.parallax.workspaceZoom
|
||||
property real minSuitableScale: 1 // Some reasonable init, to be updated
|
||||
property real effectiveWallpaperScale: minSuitableScale * parallaxRation
|
||||
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 scaledWallpaperWidth: wallpaperWidth * effectiveWallpaperScale
|
||||
property real scaledWallpaperHeight: wallpaperHeight * effectiveWallpaperScale
|
||||
property real parallaxTotalPixelsX: Math.max(0, scaledWallpaperWidth - screen.width)
|
||||
property real parallaxTotalPixelsY: Math.max(0, scaledWallpaperHeight - screen.height)
|
||||
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;
|
||||
|
||||
// Perfect image; scale = 1
|
||||
// Small picture; scale > 1; will zoom in the picture
|
||||
// Big picture; scale < 1; will zoom out the picture
|
||||
// Choose max number so every side will fit
|
||||
bgRoot.minSuitableScale = Math.max(screenWidth / width, screenHeight / height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
// Wallpaper
|
||||
StyledImage {
|
||||
id: wallpaper
|
||||
visible: opacity > 0 && !blurLoader.active
|
||||
opacity: (status === Image.Ready && !bgRoot.wallpaperIsVideo) ? 1 : 0
|
||||
cache: false
|
||||
smooth: false
|
||||
|
||||
property int workspaceIndex: (bgRoot.monitor.activeWorkspace?.id ?? 1) - 1
|
||||
property real middleFraction: 0.5
|
||||
property real fraction: {
|
||||
// 0 - start of the picture
|
||||
// 1 - end of the picture
|
||||
if (bgRoot.totalWorkspaces <= 1) {
|
||||
return middleFraction;
|
||||
}
|
||||
return Math.max(0, Math.min(1, workspaceIndex / (bgRoot.totalWorkspaces - 1)));
|
||||
}
|
||||
|
||||
property real usedFractionX: {
|
||||
let usedFraction = middleFraction;
|
||||
if (Config.options.background.parallax.enableWorkspace && !bgRoot.verticalParallax) {
|
||||
usedFraction = fraction;
|
||||
}
|
||||
if (Config.options.background.parallax.enableSidebar) {
|
||||
let sidebarFraction = bgRoot.parallaxRation / bgRoot.workspaceChunkSize / 2;
|
||||
usedFraction += (sidebarFraction * GlobalStates.sidebarRightOpen - sidebarFraction * GlobalStates.sidebarLeftOpen);
|
||||
}
|
||||
return Math.max(0, Math.min(1, usedFraction));
|
||||
}
|
||||
property real usedFractionY: {
|
||||
let usedFraction = middleFraction;
|
||||
if (Config.options.background.parallax.enableWorkspace && bgRoot.verticalParallax) {
|
||||
usedFraction = fraction;
|
||||
}
|
||||
return Math.max(0, Math.min(1, usedFraction));
|
||||
}
|
||||
|
||||
x: {
|
||||
if (bgRoot.screen.width > width) {
|
||||
// Center the picture
|
||||
return (bgRoot.screen.width - width) / 2;
|
||||
}
|
||||
return - bgRoot.parallaxTotalPixelsX * usedFractionX;
|
||||
}
|
||||
y: {
|
||||
if (bgRoot.screen.height > height) {
|
||||
// Center the picture
|
||||
return (bgRoot.screen.height - height) / 2;
|
||||
}
|
||||
return - bgRoot.parallaxTotalPixelsY * usedFractionY;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
width: bgRoot.scaledWallpaperWidth
|
||||
height: bgRoot.scaledWallpaperHeight
|
||||
}
|
||||
|
||||
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
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
readonly property real parallaxFactor: {
|
||||
var f = Config.options.background.parallax.widgetsFactor;
|
||||
return f / bgRoot.parallaxRation;
|
||||
}
|
||||
readonly property real baseWallpaperOffsetX: (bgRoot.screen.width - wallpaper.width) / 2
|
||||
readonly property real baseWallpaperOffsetY: (bgRoot.screen.height - wallpaper.height) / 2
|
||||
readonly property real wallpaperTotalOffsetX: wallpaper.x - baseWallpaperOffsetX
|
||||
readonly property real wallpaperTotalOffsetY: wallpaper.y - baseWallpaperOffsetY
|
||||
readonly property bool locked: GlobalStates.screenLocked
|
||||
x: wallpaperTotalOffsetX * parallaxFactor * !locked
|
||||
y: wallpaperTotalOffsetY * parallaxFactor * !locked
|
||||
|
||||
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
|
||||
scaledScreenHeight: bgRoot.screen.height
|
||||
wallpaperScale: 1
|
||||
}
|
||||
}
|
||||
|
||||
FadeLoader {
|
||||
shown: Config.options.background.widgets.clock.enable
|
||||
sourceComponent: ClockWidget {
|
||||
screenWidth: bgRoot.screen.width
|
||||
screenHeight: bgRoot.screen.height
|
||||
scaledScreenWidth: bgRoot.screen.width
|
||||
scaledScreenHeight: bgRoot.screen.height
|
||||
wallpaperScale: 1
|
||||
wallpaperSafetyTriggered: bgRoot.wallpaperSafetyTriggered
|
||||
}
|
||||
}
|
||||
|
||||
FadeLoader {
|
||||
shown: Config.options.background.widgets.visualizer.enable
|
||||
sourceComponent: VisualizerWidget {
|
||||
screenWidth: bgRoot.screen.width; screenHeight: bgRoot.screen.height
|
||||
scaledScreenWidth: bgRoot.screen.width; scaledScreenHeight: bgRoot.screen.height
|
||||
wallpaperScale: 1
|
||||
}
|
||||
}
|
||||
FadeLoader {
|
||||
shown: Config.options.background.widgets.stats.enable
|
||||
sourceComponent: StatsWidget {
|
||||
screenWidth: bgRoot.screen.width; screenHeight: bgRoot.screen.height
|
||||
scaledScreenWidth: bgRoot.screen.width; scaledScreenHeight: bgRoot.screen.height
|
||||
wallpaperScale: 1
|
||||
}
|
||||
}
|
||||
FadeLoader {
|
||||
shown: Config.options.background.widgets.systemResources.enable
|
||||
sourceComponent: SystemResourcesWidget {
|
||||
screenWidth: bgRoot.screen.width; screenHeight: bgRoot.screen.height
|
||||
scaledScreenWidth: bgRoot.screen.width; scaledScreenHeight: bgRoot.screen.height
|
||||
wallpaperScale: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
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) return;
|
||||
if (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,19 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
font {
|
||||
family: Appearance.font.family.expressive
|
||||
pixelSize: 20
|
||||
weight: 350
|
||||
// Set empty to prevent conflicts, not meaningless
|
||||
styleName: ""
|
||||
variableAxes: ({})
|
||||
}
|
||||
style: Text.Raised
|
||||
styleColor: Appearance.colors.colShadow
|
||||
animateChange: Config.options.background.widgets.clock.digital.animateChange
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
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: GlobalStates.screenLocked ? Config.options.background.widgets.clock.styleLocked : 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 (!Config.options.background.widgets.clock.digital.adaptiveAlignment || root.forceCenter || Config.options.background.widgets.clock.digital.vertical)
|
||||
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)
|
||||
fade: false
|
||||
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)
|
||||
fade: false
|
||||
sourceComponent: DigitalClock {
|
||||
colText: root.colText
|
||||
textHorizontalAlignment: root.textHorizontalAlignment
|
||||
}
|
||||
}
|
||||
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 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
|
||||
horizontalAlignment: root.textHorizontalAlignment
|
||||
anchors.verticalCenter: statusTextRow.verticalCenter
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.large
|
||||
weight: Font.Normal
|
||||
}
|
||||
style: Text.Raised
|
||||
styleColor: Appearance.colors.colShadow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: categoryFileView
|
||||
path: Config.ready ? Directories.generatedWallpaperCategoryPath : ""
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
root.setClockPreset(categoryFileView.text().trim())
|
||||
}
|
||||
}
|
||||
|
||||
property bool useSineCookie: Config.options.background.widgets.clock.cookie.useSineCookie
|
||||
StyledDropShadow {
|
||||
target: root.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: root.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: !root.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,71 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: clockColumn
|
||||
spacing: 4
|
||||
|
||||
property bool isVertical: Config.options.background.widgets.clock.digital.vertical
|
||||
property color colText: Appearance.colors.colOnSecondaryContainer
|
||||
property var textHorizontalAlignment: Text.AlignHCenter
|
||||
|
||||
// Time
|
||||
ClockText {
|
||||
id: timeTextTop
|
||||
text: clockColumn.isVertical ? DateTime.time.split(":")[0].padStart(2, "0") : DateTime.time
|
||||
color: clockColumn.colText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font {
|
||||
pixelSize: Config.options.background.widgets.clock.digital.font.size
|
||||
weight: Config.options.background.widgets.clock.digital.font.weight
|
||||
family: Config.options.background.widgets.clock.digital.font.family
|
||||
variableAxes: ({
|
||||
"wdth": Config.options.background.widgets.clock.digital.font.width,
|
||||
"ROND": Config.options.background.widgets.clock.digital.font.roundness
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.topMargin: -40
|
||||
Layout.fillWidth: true
|
||||
active: clockColumn.isVertical
|
||||
visible: active
|
||||
sourceComponent: ClockText {
|
||||
id: timeTextBottom
|
||||
text: DateTime.time.split(":")[1].split(" ")[0].padStart(2, "0")
|
||||
color: clockColumn.colText
|
||||
horizontalAlignment: clockColumn.textHorizontalAlignment
|
||||
font {
|
||||
pixelSize: timeTextTop.font.pixelSize
|
||||
weight: timeTextTop.font.weight
|
||||
family: timeTextTop.font.family
|
||||
variableAxes: timeTextTop.font.variableAxes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
ClockText {
|
||||
visible: Config.options.background.widgets.clock.digital.showDate
|
||||
Layout.topMargin: -20
|
||||
Layout.fillWidth: true
|
||||
text: DateTime.longDate
|
||||
color: clockColumn.colText
|
||||
horizontalAlignment: clockColumn.textHorizontalAlignment
|
||||
}
|
||||
|
||||
// Quote
|
||||
ClockText {
|
||||
visible: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text.length > 0
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
text: Config.options.background.widgets.clock.quote.text
|
||||
animateChange: false
|
||||
color: clockColumn.colText
|
||||
horizontalAlignment: clockColumn.textHorizontalAlignment
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,141 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
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: "systemResources"
|
||||
implicitHeight: backgroundShape.implicitHeight
|
||||
implicitWidth: backgroundShape.implicitWidth
|
||||
|
||||
property bool showGraphs: Config.options.background.widgets.systemResources.showGraphs || false
|
||||
property int maxHistory: ResourceUsage.historyLength
|
||||
property var gpuUsageHistory: []
|
||||
property real currentGpuUsage: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
var arr = [];
|
||||
for (var i = 0; i < maxHistory; i++) arr.push(0);
|
||||
gpuUsageHistory = arr;
|
||||
}
|
||||
|
||||
Timer { interval: 3000; running: true; repeat: true; triggeredOnStart: true; onTriggered: gpuProcess.running = true }
|
||||
|
||||
Process {
|
||||
id: gpuProcess
|
||||
running: false
|
||||
command: ["bash", "-c",
|
||||
"if command -v nvidia-smi &>/dev/null; then nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits; " +
|
||||
"elif [ -f /sys/class/drm/card0/device/gpu_busy_percent ]; then cat /sys/class/drm/card0/device/gpu_busy_percent; " +
|
||||
"else echo 0; fi"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
var v = parseFloat(line.trim());
|
||||
if (!isNaN(v)) {
|
||||
root.currentGpuUsage = v;
|
||||
var arr = [...root.gpuUsageHistory, v/100.0];
|
||||
if (arr.length > root.maxHistory) arr.shift();
|
||||
root.gpuUsageHistory = arr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledDropShadow { target: backgroundShape }
|
||||
|
||||
Rectangle {
|
||||
id: backgroundShape
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.windowRounding
|
||||
color: Appearance.colors.colPrimaryContainer
|
||||
implicitWidth: 350
|
||||
implicitHeight: contentCol.implicitHeight + 40
|
||||
|
||||
Column {
|
||||
id: contentCol
|
||||
anchors.centerIn: parent
|
||||
spacing: 20
|
||||
width: parent.width - 40
|
||||
|
||||
Row {
|
||||
spacing: 15
|
||||
MaterialSymbol { iconSize: 32; color: Appearance.colors.colOnPrimaryContainer; text: "memory"; anchors.verticalCenter: parent.verticalCenter }
|
||||
StyledText { font.pixelSize: 18; font.weight: Font.Bold; color: Appearance.colors.colOnPrimaryContainer; text: "System Resources"; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 30
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Column {
|
||||
StyledText { text: "CPU"; font.pixelSize: 12; color: Appearance.colors.colPrimary }
|
||||
StyledText { text: Math.round(ResourceUsage.cpuUsage * 100) + "%"; font.pixelSize: 18; color: Appearance.colors.colOnPrimaryContainer; font.weight: Font.Bold }
|
||||
}
|
||||
Column {
|
||||
StyledText { text: "RAM"; font.pixelSize: 12; color: Appearance.colors.colPrimary }
|
||||
StyledText { text: Math.round(ResourceUsage.memoryUsedPercentage * 100) + "%"; font.pixelSize: 18; color: Appearance.colors.colOnPrimaryContainer; font.weight: Font.Bold }
|
||||
}
|
||||
Column {
|
||||
StyledText { text: "GPU"; font.pixelSize: 12; color: Appearance.colors.colPrimary }
|
||||
StyledText { text: Math.round(root.currentGpuUsage) + "%"; font.pixelSize: 18; color: Appearance.colors.colOnPrimaryContainer; font.weight: Font.Bold }
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: graphCanvas
|
||||
width: parent.width; height: 100; visible: root.showGraphs
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
function drawSmooth(arr, color) {
|
||||
if (!arr || arr.length < 2) return;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = color; ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round"; ctx.lineJoin = "round";
|
||||
var n = arr.length, step = width/(n-1);
|
||||
ctx.moveTo(0, height - arr[0]*(height-4) - 2);
|
||||
for (var i=1; i<n; i++) {
|
||||
var cpx = ((i-1)*step + i*step)/2;
|
||||
var py = height - arr[i-1]*(height-4) - 2;
|
||||
var cy = height - arr[i]*(height-4) - 2;
|
||||
ctx.bezierCurveTo(cpx, py, cpx, cy, i*step, cy);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawSmooth(root.gpuUsageHistory, Appearance.colors.colError);
|
||||
drawSmooth(ResourceUsage.memoryUsageHistory, Appearance.colors.colSecondary);
|
||||
drawSmooth(ResourceUsage.cpuUsageHistory, Appearance.colors.colPrimary);
|
||||
}
|
||||
Timer { interval: 1000; running: root.showGraphs; repeat: true; onTriggered: parent.requestPaint() }
|
||||
|
||||
Row {
|
||||
anchors.top: parent.bottom; anchors.topMargin: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter; spacing: 15
|
||||
Repeater {
|
||||
model: [
|
||||
{ label: "CPU", color: Appearance.colors.colPrimary },
|
||||
{ label: "RAM", color: Appearance.colors.colSecondary },
|
||||
{ label: "GPU", color: Appearance.colors.colError }
|
||||
]
|
||||
Row {
|
||||
spacing: 5
|
||||
required property var modelData
|
||||
Rectangle { width: 10; height: 10; radius: 5; color: modelData.color; anchors.verticalCenter: parent.verticalCenter }
|
||||
StyledText { text: modelData.label; font.pixelSize: 10; color: Appearance.colors.colOnPrimaryContainer }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: 1; height: root.showGraphs ? 20 : 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
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: "stats"
|
||||
implicitHeight: backgroundShape.implicitHeight
|
||||
implicitWidth: backgroundShape.implicitWidth
|
||||
|
||||
property string githubUsername: Config.options.background.widgets.stats.githubUsername
|
||||
property string codeforcesUsername: Config.options.background.widgets.stats.codeforcesUsername
|
||||
property bool showGraphs: Config.options.background.widgets.stats.showGraphs || false
|
||||
|
||||
property int ghFollowers: 0
|
||||
property int ghRepos: 0
|
||||
property string cfRank: "--"
|
||||
property int cfRating: 0
|
||||
property var githubActivityArray: []
|
||||
property var cfActivityArray: []
|
||||
property int maxGithubActivity: 1
|
||||
property int maxCfActivity: 1
|
||||
|
||||
function fetchGithub() {
|
||||
if (!githubUsername) return;
|
||||
var req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState === 4 && req.status === 200) {
|
||||
var data = JSON.parse(req.responseText);
|
||||
ghFollowers = data.followers || 0;
|
||||
ghRepos = data.public_repos || 0;
|
||||
}
|
||||
}
|
||||
req.open("GET", "https://api.github.com/users/" + githubUsername, true);
|
||||
req.send();
|
||||
|
||||
if (showGraphs) {
|
||||
var ereq = new XMLHttpRequest();
|
||||
ereq.onreadystatechange = function() {
|
||||
if (ereq.readyState !== 4 || ereq.status !== 200) return;
|
||||
var events = JSON.parse(ereq.responseText);
|
||||
var bins = new Array(30).fill(0), maxV = 0, now = new Date();
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var d = Math.floor(Math.abs(now - new Date(events[i].created_at)) / 86400000);
|
||||
if (d < 30) { bins[29-d]++; if (bins[29-d] > maxV) maxV = bins[29-d]; }
|
||||
}
|
||||
maxGithubActivity = Math.max(1, maxV);
|
||||
githubActivityArray = bins;
|
||||
}
|
||||
ereq.open("GET", "https://api.github.com/users/" + githubUsername + "/events/public?per_page=100", true);
|
||||
ereq.send();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchCodeforces() {
|
||||
if (!codeforcesUsername) return;
|
||||
var req = new XMLHttpRequest();
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState === 4 && req.status === 200) {
|
||||
var data = JSON.parse(req.responseText);
|
||||
if (data.status === "OK" && data.result.length > 0) {
|
||||
cfRank = data.result[0].rank || "--";
|
||||
cfRating = data.result[0].rating || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
req.open("GET", "https://codeforces.com/api/user.info?handles=" + codeforcesUsername, true);
|
||||
req.send();
|
||||
|
||||
if (showGraphs) {
|
||||
var sreq = new XMLHttpRequest();
|
||||
sreq.onreadystatechange = function() {
|
||||
if (sreq.readyState !== 4 || sreq.status !== 200) return;
|
||||
var data = JSON.parse(sreq.responseText);
|
||||
if (data.status !== "OK") return;
|
||||
var bins = new Array(30).fill(0), maxV = 0, nowS = Date.now()/1000;
|
||||
for (var i = 0; i < data.result.length; i++) {
|
||||
var d = Math.floor((nowS - data.result[i].creationTimeSeconds) / 86400);
|
||||
if (d < 30) { bins[29-d]++; if (bins[29-d] > maxV) maxV = bins[29-d]; }
|
||||
}
|
||||
maxCfActivity = Math.max(1, maxV);
|
||||
cfActivityArray = bins;
|
||||
}
|
||||
sreq.open("GET", "https://codeforces.com/api/user.status?handle=" + codeforcesUsername + "&from=1&count=200", true);
|
||||
sreq.send();
|
||||
}
|
||||
}
|
||||
|
||||
Timer { interval: 600000; running: true; repeat: true; triggeredOnStart: true; onTriggered: { fetchGithub(); fetchCodeforces(); } }
|
||||
onGithubUsernameChanged: fetchGithub()
|
||||
onCodeforcesUsernameChanged: fetchCodeforces()
|
||||
onShowGraphsChanged: { if (showGraphs) { fetchGithub(); fetchCodeforces(); } }
|
||||
|
||||
StyledDropShadow { target: backgroundShape }
|
||||
|
||||
Rectangle {
|
||||
id: backgroundShape
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.windowRounding
|
||||
color: Appearance.colors.colPrimaryContainer
|
||||
implicitWidth: 320
|
||||
implicitHeight: contentCol.implicitHeight + 40
|
||||
|
||||
Column {
|
||||
id: contentCol
|
||||
anchors.centerIn: parent
|
||||
spacing: 20
|
||||
width: parent.width - 40
|
||||
|
||||
// GitHub
|
||||
Row {
|
||||
spacing: 15
|
||||
MaterialSymbol { iconSize: 40; color: Appearance.colors.colOnPrimaryContainer; text: "code"; anchors.verticalCenter: parent.verticalCenter }
|
||||
Column {
|
||||
StyledText { font.pixelSize: 18; font.weight: Font.Bold; color: Appearance.colors.colOnPrimaryContainer; text: "GitHub: " + (githubUsername || "Not set") }
|
||||
StyledText { font.pixelSize: 14; color: Appearance.colors.colPrimary; text: ghFollowers + " Followers • " + ghRepos + " Repos" }
|
||||
}
|
||||
}
|
||||
Canvas {
|
||||
width: parent.width; height: root.showGraphs ? 40 : 0; visible: root.showGraphs
|
||||
onPaint: {
|
||||
var ctx = getContext("2d"); ctx.clearRect(0,0,width,height);
|
||||
var arr = root.githubActivityArray;
|
||||
if (!arr || arr.length < 2) return;
|
||||
ctx.beginPath(); ctx.strokeStyle = Appearance.colors.colSecondary;
|
||||
ctx.lineWidth = 2; ctx.lineCap = "round"; ctx.lineJoin = "round";
|
||||
var step = width/(arr.length-1);
|
||||
ctx.moveTo(0, height - (arr[0]/root.maxGithubActivity)*(height-4)-2);
|
||||
for(var i=1;i<arr.length;i++){
|
||||
var cpx=((i-1)*step+i*step)/2;
|
||||
var py=height-(arr[i-1]/root.maxGithubActivity)*(height-4)-2;
|
||||
var cy=height-(arr[i]/root.maxGithubActivity)*(height-4)-2;
|
||||
ctx.bezierCurveTo(cpx,py,cpx,cy,i*step,cy);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
Timer { interval: 1000; running: root.showGraphs; repeat: true; onTriggered: parent.requestPaint() }
|
||||
}
|
||||
|
||||
// Codeforces
|
||||
Row {
|
||||
spacing: 15
|
||||
MaterialSymbol { iconSize: 40; color: Appearance.colors.colOnPrimaryContainer; text: "bar_chart"; anchors.verticalCenter: parent.verticalCenter }
|
||||
Column {
|
||||
StyledText { font.pixelSize: 18; font.weight: Font.Bold; color: Appearance.colors.colOnPrimaryContainer; text: "Codeforces: " + (codeforcesUsername || "Not set") }
|
||||
StyledText { font.pixelSize: 14; color: Appearance.colors.colPrimary; text: "Rating: " + cfRating + " • " + cfRank }
|
||||
}
|
||||
}
|
||||
Canvas {
|
||||
width: parent.width; height: root.showGraphs ? 40 : 0; visible: root.showGraphs
|
||||
onPaint: {
|
||||
var ctx = getContext("2d"); ctx.clearRect(0,0,width,height);
|
||||
var arr = root.cfActivityArray;
|
||||
if (!arr || arr.length < 2) return;
|
||||
ctx.beginPath(); ctx.strokeStyle = Appearance.colors.colError;
|
||||
ctx.lineWidth = 2; ctx.lineCap = "round"; ctx.lineJoin = "round";
|
||||
var step = width/(arr.length-1);
|
||||
ctx.moveTo(0, height - (arr[0]/root.maxCfActivity)*(height-4)-2);
|
||||
for(var i=1;i<arr.length;i++){
|
||||
var cpx=((i-1)*step+i*step)/2;
|
||||
var py=height-(arr[i-1]/root.maxCfActivity)*(height-4)-2;
|
||||
var cy=height-(arr[i]/root.maxCfActivity)*(height-4)-2;
|
||||
ctx.bezierCurveTo(cpx,py,cpx,cy,i*step,cy);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
Timer { interval: 1000; running: root.showGraphs; repeat: true; onTriggered: parent.requestPaint() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
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: "visualizer"
|
||||
implicitHeight: backgroundShape.implicitHeight
|
||||
implicitWidth: backgroundShape.implicitWidth
|
||||
|
||||
property int numBars: {
|
||||
// cava requires even bar count in stereo mode — force even
|
||||
var raw = Config.options.background.widgets.visualizer.bars;
|
||||
return raw % 2 === 0 ? raw : raw + 1;
|
||||
}
|
||||
property bool isVertical: Config.options.background.widgets.visualizer.vertical
|
||||
property string style: Config.options.background.widgets.visualizer.style
|
||||
property var amplitudeArray: []
|
||||
property bool cavaReady: false
|
||||
|
||||
// Cava config path — /tmp is cleared on reboot, we always regenerate it
|
||||
readonly property string cavaConfigPath: "/tmp/quickshell_cava_config"
|
||||
|
||||
function buildCavaConfig() {
|
||||
return "[general]\nbars = " + root.numBars + "\nchannels = mono\n" +
|
||||
"[output]\nmethod = raw\nraw_target = /dev/stdout\n" +
|
||||
"data_format = ascii\nascii_max_range = 100\n";
|
||||
}
|
||||
|
||||
// Souveraine (idle-power task 4): cava only earns its keep while the
|
||||
// widget can actually be seen. Gate the watchdog on visibility + display
|
||||
// activity, and tear cava down when either goes away.
|
||||
readonly property bool wantCava: parent.visible && GlobalStates.displayActive
|
||||
onWantCavaChanged: {
|
||||
if (!wantCava && cavaProcess.running) {
|
||||
root.cavaReady = false;
|
||||
cavaProcess.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Watchdog: every 2s, if cava isn't running, write config and start it
|
||||
Timer {
|
||||
id: watchdog
|
||||
interval: 2000
|
||||
running: root.wantCava
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
if (!cavaProcess.running) {
|
||||
root.cavaReady = false;
|
||||
writeConfigProcess.running = false;
|
||||
writeConfigProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Write the cava config to /tmp
|
||||
Process {
|
||||
id: writeConfigProcess
|
||||
running: false
|
||||
command: ["bash", "-c",
|
||||
"printf '%s' '" + root.buildCavaConfig().replace(/'/g, "'\\''") + "' > " + root.cavaConfigPath]
|
||||
onExited: (code) => {
|
||||
if (code === 0) startDelay.start();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: startDelay
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
cavaProcess.running = true;
|
||||
root.cavaReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Run cava, read its ascii stdout
|
||||
Process {
|
||||
id: cavaProcess
|
||||
running: false
|
||||
command: ["cava", "-p", root.cavaConfigPath]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
var vals = line.split(';')
|
||||
.map(s => parseInt(s, 10))
|
||||
.filter(v => !isNaN(v) && v >= 0);
|
||||
if (vals.length > 0) root.amplitudeArray = vals;
|
||||
}
|
||||
}
|
||||
onRunningChanged: {
|
||||
if (!running) root.cavaReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
// When bar count changes, restart cava with new config
|
||||
onNumBarsChanged: {
|
||||
cavaProcess.running = false;
|
||||
writeConfigProcess.running = false;
|
||||
writeConfigProcess.running = true;
|
||||
var arr = [];
|
||||
for (let i = 0; i < numBars; i++) arr.push(0);
|
||||
amplitudeArray = arr;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: backgroundShape
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
implicitWidth: isVertical ? 200 : (root.numBars * 10 + 40)
|
||||
implicitHeight: isVertical ? (root.numBars * 10 + 40) : 200
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
|
||||
// Horizontal base line
|
||||
Rectangle {
|
||||
visible: !isVertical
|
||||
color: Appearance.colors.colPrimary
|
||||
height: 4; width: parent.width
|
||||
anchors.bottom: parent.bottom
|
||||
radius: 2
|
||||
}
|
||||
// Vertical base line
|
||||
Rectangle {
|
||||
visible: isVertical
|
||||
color: Appearance.colors.colPrimary
|
||||
width: 4; height: parent.height
|
||||
anchors.left: parent.left
|
||||
radius: 2
|
||||
}
|
||||
|
||||
// Horizontal bars
|
||||
Row {
|
||||
anchors.fill: parent; spacing: 4
|
||||
visible: !isVertical && root.style === "bars"
|
||||
Repeater {
|
||||
model: root.amplitudeArray
|
||||
Rectangle {
|
||||
required property int modelData
|
||||
width: Math.max(1, parent.width / root.amplitudeArray.length - parent.spacing)
|
||||
height: Math.max(2, (modelData / 100) * parent.height)
|
||||
anchors.bottom: parent.bottom
|
||||
color: Appearance.colors.colPrimary; radius: 2
|
||||
Behavior on height { NumberAnimation { duration: 60; easing.type: Easing.OutSine } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical bars
|
||||
Column {
|
||||
anchors.fill: parent; spacing: 4
|
||||
visible: isVertical && root.style === "bars"
|
||||
Repeater {
|
||||
model: root.amplitudeArray
|
||||
Rectangle {
|
||||
required property int modelData
|
||||
height: Math.max(1, parent.height / root.amplitudeArray.length - parent.spacing)
|
||||
width: Math.max(2, (modelData / 100) * parent.width)
|
||||
anchors.left: parent.left
|
||||
color: Appearance.colors.colPrimary; radius: 2
|
||||
Behavior on width { NumberAnimation { duration: 60; easing.type: Easing.OutSine } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wave — smooth bezier curve
|
||||
Canvas {
|
||||
anchors.fill: parent
|
||||
visible: root.style === "wave"
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
var arr = root.amplitudeArray;
|
||||
if (arr.length < 2) return;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = Appearance.colors.colPrimary;
|
||||
ctx.lineWidth = 4; ctx.lineCap = "round"; ctx.lineJoin = "round";
|
||||
if (!isVertical) {
|
||||
var stepX = width / (arr.length - 1);
|
||||
ctx.moveTo(0, height - (arr[0]/100)*height);
|
||||
for(var i=1; i<arr.length; i++) {
|
||||
var cpx = ((i-1)*stepX + i*stepX) / 2;
|
||||
ctx.bezierCurveTo(cpx, height-(arr[i-1]/100)*height, cpx, height-(arr[i]/100)*height, i*stepX, height-(arr[i]/100)*height);
|
||||
}
|
||||
} else {
|
||||
var stepY = height / (arr.length - 1);
|
||||
ctx.moveTo((arr[0]/100)*width, 0);
|
||||
for(var j=1; j<arr.length; j++) {
|
||||
var cpy = ((j-1)*stepY + j*stepY) / 2;
|
||||
ctx.bezierCurveTo((arr[j-1]/100)*width, cpy, (arr[j]/100)*width, cpy, (arr[j]/100)*width, j*stepY);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
// 30fps repaint only while this canvas is actually visible.
|
||||
// Was `running: root.style === "wave"` — but the Canvas's own
|
||||
// `visible` (line above) already encodes that, AND collapses to
|
||||
// false when the widget subtree is hidden. Gating the timer on
|
||||
// `parent.visible` stops the continuous redraw whenever the
|
||||
// canvas isn't shown, instead of painting behind a hidden
|
||||
// background. No new import needed (pure QtQuick `visible`).
|
||||
Timer { interval: 33; running: parent.visible; repeat: true; onTriggered: parent.requestPaint() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
surfaces/quickshell/ii-base/modules/ii/bar/ActiveWindow.qml
Normal file
66
surfaces/quickshell/ii-base/modules/ii/bar/ActiveWindow.qml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
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 bool focusingThisMonitor: HyprlandData.activeWorkspace?.monitor == monitor?.name
|
||||
property var hyprlandDataMonitor: HyprlandData.monitors.find(m => m.id === root.monitor?.id)
|
||||
property bool activeIsSpecialWorkspace: Boolean(hyprlandDataMonitor?.specialWorkspace.id)
|
||||
property var currentWorkspaceID: activeIsSpecialWorkspace? hyprlandDataMonitor?.specialWorkspace.id : hyprlandDataMonitor?.activeWorkspace.id
|
||||
property var currentWorkspaceName: activeIsSpecialWorkspace?
|
||||
capitalize(hyprlandDataMonitor?.specialWorkspace.name.replace("special:", "")) :
|
||||
hyprlandDataMonitor?.activeWorkspace.name
|
||||
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(currentWorkspaceID)
|
||||
|
||||
function capitalize(s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
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 && HyprlandData.activeWindow?.workspace?.id === root.currentWorkspaceID ?
|
||||
HyprlandData.activeWindow.class :
|
||||
root.biggestWindow?.class ??
|
||||
(activeIsSpecialWorkspace ?
|
||||
Translation.tr("Scratchpad") :
|
||||
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 && HyprlandData.activeWindow?.workspace?.id === root.currentWorkspaceID
|
||||
? HyprlandData.activeWindow.title
|
||||
: root.biggestWindow?.title ??
|
||||
(activeIsSpecialWorkspace
|
||||
? `${Translation.tr("Workspace")} ${currentWorkspaceName}`
|
||||
: `${Translation.tr("Workspace")} ${monitor?.activeWorkspace?.id ?? 1}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
269
surfaces/quickshell/ii-base/modules/ii/bar/Bar.qml
Normal file
269
surfaces/quickshell/ii-base/modules/ii/bar/Bar.qml
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
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
|
||||
// Normal, not Ignore. Ignore tells the compositor to disregard
|
||||
// the exclusive zone entirely, so the careful expression below
|
||||
// computed a height that was then thrown away and every window
|
||||
// was laid out underneath the bar. The bar is not app space
|
||||
// when it is visible; that is the whole point of a zone.
|
||||
//
|
||||
// The auto-hide case still yields 0 below, so a bar configured
|
||||
// to hide still gives its space back — the mode says "honour
|
||||
// what I claim", and claiming nothing remains available.
|
||||
exclusionMode: ExclusionMode.Normal
|
||||
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"
|
||||
|
||||
// Positioning
|
||||
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
|
||||
}
|
||||
|
||||
// Include in focus grab
|
||||
Component.onCompleted: {
|
||||
GlobalFocusGrab.addPersistent(barRoot);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
GlobalFocusGrab.removePersistent(barRoot);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
357
surfaces/quickshell/ii-base/modules/ii/bar/BarContent.qml
Normal file
357
surfaces/quickshell/ii-base/modules/ii/bar/BarContent.qml
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
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: Brightness.decreaseBrightness()
|
||||
onScrollUp: Brightness.increaseBrightness()
|
||||
onMovedAway: GlobalStates.osdBrightnessOpen = false
|
||||
onPressed: event => {
|
||||
if (event.button === Qt.LeftButton)
|
||||
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
|
||||
}
|
||||
|
||||
// Visual content
|
||||
ScrollHint {
|
||||
reveal: barLeftSideMouseArea.hovered
|
||||
icon: Hyprsunset.gamma === 100 ? "light_mode" : "wb_twilight"
|
||||
tooltipText: Translation.tr("Scroll to change brightness")
|
||||
side: "left"
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: leftSectionRowLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
LeftSidebarButton { // Left sidebar button
|
||||
id: leftSidebarButton
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Appearance.rounding.screenRounding
|
||||
colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
|
||||
}
|
||||
|
||||
ActiveWindow {
|
||||
Layout.leftMargin: 10 + (leftSidebarButton.visible ? 0 : Appearance.rounding.screenRounding)
|
||||
Layout.rightMargin: Appearance.rounding.screenRounding
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: root.useShortenedForm === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row { // Middle section
|
||||
id: middleSection
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
spacing: 4
|
||||
|
||||
BarGroup {
|
||||
id: leftCenterGroup
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Resources {
|
||||
alwaysShowAllResources: root.useShortenedForm === 2
|
||||
Layout.fillWidth: root.useShortenedForm === 2
|
||||
}
|
||||
|
||||
Media {
|
||||
visible: root.useShortenedForm < 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.bar.claudeUsage.enable
|
||||
visible: root.useShortenedForm < 2 && active
|
||||
sourceComponent: ClaudeUsageBar {}
|
||||
}
|
||||
}
|
||||
|
||||
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: rightCenterGroupContent.implicitWidth
|
||||
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: Audio.decrementVolume();
|
||||
onScrollUp: Audio.incrementVolume();
|
||||
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 {
|
||||
text: Network.materialSymbol
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
MaterialSymbol {
|
||||
Layout.leftMargin: indicatorsRowLayout.realSpacing
|
||||
visible: BluetoothStatus.available
|
||||
text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Revealer {
|
||||
reveal: TimerService.pomodoroRunning
|
||||
Layout.fillHeight: true
|
||||
|
||||
PomodoroBarIndicator {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
surfaces/quickshell/ii-base/modules/ii/bar/BarGroup.qml
Normal file
41
surfaces/quickshell/ii-base/modules/ii/bar/BarGroup.qml
Normal file
|
|
@ -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,63 @@
|
|||
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: !Config.options.bar.tooltips.clickToShow
|
||||
|
||||
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 {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: (parent.height - height) / 2
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
72
surfaces/quickshell/ii-base/modules/ii/bar/BatteryPopup.qml
Normal file
72
surfaces/quickshell/ii-base/modules/ii/bar/BatteryPopup.qml
Normal file
|
|
@ -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
|
||||
|
||||
}
|
||||
153
surfaces/quickshell/ii-base/modules/ii/bar/ClaudeUsageBar.qml
Normal file
153
surfaces/quickshell/ii-base/modules/ii/bar/ClaudeUsageBar.qml
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import qs
|
||||
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
|
||||
|
||||
implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
hoverEnabled: !Config.options.bar.tooltips.clickToShow
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
|
||||
// Which window the single gauge shows; toggled by clicking.
|
||||
property bool showingWeekly: Config.options.bar.claudeUsage.defaultWeekly
|
||||
|
||||
onPressed: event => {
|
||||
if (event.button === Qt.MiddleButton)
|
||||
ClaudeUsage.getData(); // middle-click to refresh now
|
||||
else
|
||||
root.showingWeekly = !root.showingWeekly; // click to switch session <-> week
|
||||
}
|
||||
|
||||
// A single Claude-usage gauge: icon inside a ring + percentage number.
|
||||
component UsageGauge: Item {
|
||||
id: gauge
|
||||
required property string iconName
|
||||
required property real percentage // 0..1
|
||||
property string label: "" // short window identifier, e.g. "5h" / "7d"
|
||||
property int warningThreshold: Config.options.bar.claudeUsage.warningThreshold
|
||||
property bool warning: percentage * 100 >= warningThreshold
|
||||
|
||||
implicitWidth: gaugeRow.implicitWidth
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
|
||||
RowLayout {
|
||||
id: gaugeRow
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
ClippedFilledCircularProgress {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
lineWidth: Appearance.rounding.unsharpen
|
||||
value: Math.max(0, Math.min(1, gauge.percentage))
|
||||
implicitSize: 20
|
||||
colPrimary: gauge.warning ? Appearance.colors.colError : Appearance.colors.colOnSecondaryContainer
|
||||
accountForLightBleeding: !gauge.warning
|
||||
enableAnimation: false
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: 20
|
||||
height: 20
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
font.weight: Font.DemiBold
|
||||
fill: 1
|
||||
text: gauge.iconName
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: Appearance.colors.colOnLayer1
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
text: ClaudeUsage.available ? `${Math.round(gauge.percentage * 100)}` : "–"
|
||||
}
|
||||
|
||||
StyledText { // which window is being shown
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: gauge.label.length > 0
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
text: gauge.label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
spacing: 6
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4
|
||||
anchors.rightMargin: 4
|
||||
|
||||
UsageGauge { // Click the module to switch between the two windows
|
||||
iconName: root.showingWeekly ? "calendar_month" : "auto_awesome"
|
||||
percentage: (root.showingWeekly ? ClaudeUsage.sevenDay : ClaudeUsage.fiveHour) / 100
|
||||
label: root.showingWeekly ? "7d" : "5h"
|
||||
}
|
||||
}
|
||||
|
||||
StyledPopup {
|
||||
hoverTarget: root
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
StyledPopupHeaderRow {
|
||||
icon: "auto_awesome"
|
||||
label: ClaudeUsage.subscriptionType.length > 0 ? `Claude ${ClaudeUsage.subscriptionType.charAt(0).toUpperCase() + ClaudeUsage.subscriptionType.slice(1)}` : "Claude usage"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
visible: ClaudeUsage.available
|
||||
|
||||
StyledPopupValueRow {
|
||||
icon: "timer"
|
||||
label: Translation.tr("Session (5h):")
|
||||
value: `${Math.round(ClaudeUsage.fiveHour)}% · ${ClaudeUsage.timeUntil(ClaudeUsage.fiveHourReset)}`
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
icon: "calendar_month"
|
||||
label: Translation.tr("Week (7d):")
|
||||
value: `${Math.round(ClaudeUsage.sevenDay)}% · ${ClaudeUsage.timeUntil(ClaudeUsage.sevenDayReset)}`
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
visible: ClaudeUsage.sevenDayOpus >= 0
|
||||
icon: "neurology"
|
||||
label: Translation.tr("Week · Opus:")
|
||||
value: `${Math.round(ClaudeUsage.sevenDayOpus)}%`
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
visible: ClaudeUsage.sevenDaySonnet >= 0
|
||||
icon: "graph_3"
|
||||
label: Translation.tr("Week · Sonnet:")
|
||||
value: `${Math.round(ClaudeUsage.sevenDaySonnet)}%`
|
||||
}
|
||||
StyledPopupValueRow {
|
||||
visible: ClaudeUsage.extraEnabled && ClaudeUsage.extraMonthlyLimit > 0
|
||||
icon: "paid"
|
||||
label: Translation.tr("Extra credits:")
|
||||
value: `${ClaudeUsage.extraUsedCredits.toFixed(2)} / ${ClaudeUsage.extraMonthlyLimit} ${ClaudeUsage.extraCurrency}`
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !ClaudeUsage.available
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
text: ClaudeUsage.lastError.length > 0 ? Translation.tr("Unavailable: %1").arg(ClaudeUsage.lastError) : Translation.tr("Loading…")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
surfaces/quickshell/ii-base/modules/ii/bar/ClockWidget.qml
Normal file
49
surfaces/quickshell/ii-base/modules/ii/bar/ClockWidget.qml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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: !Config.options.bar.tooltips.clickToShow
|
||||
|
||||
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,83 @@
|
|||
import QtQuick
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
|
||||
property bool showPing: false
|
||||
|
||||
property bool aiChatEnabled: Config.options.policies.ai !== 0
|
||||
property bool translatorEnabled: Config.options.sidebar.translator.enable
|
||||
property bool animeEnabled: Config.options.policies.weeb !== 0
|
||||
visible: aiChatEnabled || translatorEnabled || animeEnabled
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
surfaces/quickshell/ii-base/modules/ii/bar/Media.qml
Normal file
89
surfaces/quickshell/ii-base/modules/ii/bar/Media.qml
Normal file
|
|
@ -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,113 @@
|
|||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
|
||||
property bool vertical: false
|
||||
readonly property color stateColor: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainer : Appearance.colors.colSecondaryContainer
|
||||
readonly property color stateColorHover: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainerHover : Appearance.colors.colSecondaryContainerHover
|
||||
readonly property color stateColorActive: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainerActive : Appearance.colors.colSecondaryContainerActive
|
||||
readonly property color stateTextColor: TimerService.pomodoroBreak ? Appearance.colors.colOnTertiaryContainer : Appearance.colors.colOnSecondaryContainer
|
||||
|
||||
implicitWidth: vertical ? Appearance.sizes.verticalBarWidth - 10 : 104
|
||||
implicitHeight: vertical ? 54 : Appearance.sizes.baseBarHeight - 8
|
||||
|
||||
buttonRadius: Appearance.rounding.full
|
||||
toggled: TimerService.pomodoroRunning
|
||||
colBackground: stateColor
|
||||
colBackgroundHover: stateColorHover
|
||||
colRipple: stateColorActive
|
||||
colBackgroundToggled: stateColor
|
||||
colBackgroundToggledHover: stateColorHover
|
||||
colRippleToggled: stateColorActive
|
||||
|
||||
function pomodoroTimeText() {
|
||||
const minutes = Math.floor(TimerService.pomodoroSecondsLeft / 60).toString().padStart(2, "0");
|
||||
const seconds = Math.floor(TimerService.pomodoroSecondsLeft % 60).toString().padStart(2, "0");
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
function compactMinutesText() {
|
||||
return Math.ceil(TimerService.pomodoroSecondsLeft / 60).toString().padStart(2, "0");
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
Persistent.states.sidebar.bottomGroup.collapsed = false;
|
||||
Persistent.states.sidebar.bottomGroup.tab = 2;
|
||||
GlobalStates.sidebarRightOpen = true;
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
visible: !root.vertical
|
||||
spacing: 5
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: TimerService.pomodoroBreak ? "coffee" : "search_activity"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: root.stateTextColor
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 48
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.pomodoroTimeText()
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
font.weight: Font.DemiBold
|
||||
color: root.stateTextColor
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
visible: root.vertical
|
||||
spacing: 1
|
||||
|
||||
ClippedFilledCircularProgress {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitSize: 28
|
||||
lineWidth: 3
|
||||
value: TimerService.pomodoroSecondsLeft / TimerService.pomodoroLapDuration
|
||||
colPrimary: root.stateTextColor
|
||||
colSecondary: root.stateColorHover
|
||||
enableAnimation: true
|
||||
|
||||
textMask: Item {
|
||||
width: 28
|
||||
height: 28
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: root.compactMinutesText()
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: TimerService.pomodoroBreak ? "coffee" : "search_activity"
|
||||
iconSize: 14
|
||||
color: root.stateTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
text: TimerService.pomodoroLongBreak ? Translation.tr("Long break") : TimerService.pomodoroBreak ? Translation.tr("Break") : Translation.tr("Pomodoro")
|
||||
}
|
||||
}
|
||||
92
surfaces/quickshell/ii-base/modules/ii/bar/Resource.qml
Normal file
92
surfaces/quickshell/ii-base/modules/ii/bar/Resource.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
138
surfaces/quickshell/ii-base/modules/ii/bar/Resources.qml
Normal file
138
surfaces/quickshell/ii-base/modules/ii/bar/Resources.qml
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
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
|
||||
property bool alwaysShowAllResources: false
|
||||
implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
hoverEnabled: !Config.options.bar.tooltips.clickToShow
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
spacing: 0
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4
|
||||
anchors.rightMargin: 4
|
||||
|
||||
Item {
|
||||
visible: NetworkTraffic.available
|
||||
implicitWidth: speedMeasure.implicitWidth
|
||||
implicitHeight: Appearance.sizes.barHeight
|
||||
Layout.rightMargin: visible ? 16 : 0
|
||||
clip: true
|
||||
|
||||
TextMetrics {
|
||||
id: speedTextMetrics
|
||||
text: "8888G/s"
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
font.family: Appearance.font.family.main
|
||||
font.variableAxes: Appearance.font.variableAxes.main
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: speedMeasure
|
||||
visible: false
|
||||
|
||||
MaterialSymbol {
|
||||
text: "south"
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: speedTextMetrics.width
|
||||
implicitHeight: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: 2
|
||||
implicitHeight: 1
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
text: "north"
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: speedTextMetrics.width
|
||||
implicitHeight: 1
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: speedRow
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
MaterialSymbol {
|
||||
text: "south"
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: speedTextMetrics.width
|
||||
text: NetworkTraffic.downloadSpeedCompactText
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer1
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideLeft
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
text: "north"
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer1
|
||||
Layout.leftMargin: 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: speedTextMetrics.width
|
||||
text: NetworkTraffic.uploadSpeedCompactText
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer1
|
||||
horizontalAlignment: Text.AlignRight
|
||||
elide: Text.ElideLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
surfaces/quickshell/ii-base/modules/ii/bar/ScrollHint.qml
Normal file
64
surfaces/quickshell/ii-base/modules/ii/bar/ScrollHint.qml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
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
|
||||
anchorEdges: Config.options.bar.bottom ? Edges.Top : Edges.Bottom
|
||||
anchorGravity: anchorEdges
|
||||
verticalMargin: 8
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
surfaces/quickshell/ii-base/modules/ii/bar/StyledPopup.qml
Normal file
86
surfaces/quickshell/ii-base/modules/ii/bar/StyledPopup.qml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
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) {
|
||||
const rawX = root.QsWindow?.mapFromItem(
|
||||
root.hoverTarget,
|
||||
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
|
||||
).x ?? 0;
|
||||
const screenWidth = root.QsWindow?.window?.screen?.width ?? 0;
|
||||
const maxX = screenWidth - popupBackground.implicitWidth - Appearance.sizes.elevationMargin * 2;
|
||||
return screenWidth > 0 ? Math.max(0, Math.min(rawX, maxX)) : rawX;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
157
surfaces/quickshell/ii-base/modules/ii/bar/SysTray.qml
Normal file
157
surfaces/quickshell/ii-base/modules/ii/bar/SysTray.qml
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
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 list<var> pinnedItems: TrayService.pinnedItems
|
||||
property list<var> unpinnedItems: TrayService.unpinnedItems
|
||||
onUnpinnedItemsChanged: {
|
||||
if (unpinnedItems.length == 0) root.closeOverflowMenu();
|
||||
}
|
||||
|
||||
function grabFocus() {
|
||||
focusGrab.active = true;
|
||||
}
|
||||
|
||||
function setExtraWindowAndGrabFocus(window) {
|
||||
if (root.activeMenu && root.activeMenu !== window) {
|
||||
if (typeof root.activeMenu.close === "function")
|
||||
root.activeMenu.close();
|
||||
root.activeMenu = null;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
106
surfaces/quickshell/ii-base/modules/ii/bar/SysTrayItem.qml
Normal file
106
surfaces/quickshell/ii-base/modules/ii/bar/SysTrayItem.qml
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
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)
|
||||
if (menu.active && menu.item && typeof menu.item.close === "function")
|
||||
menu.item.close();
|
||||
else
|
||||
menu.open();
|
||||
break;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
onEntered: {
|
||||
tooltip.text = TrayService.getTooltipForItem(root.item);
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: menu
|
||||
function open() {
|
||||
menu.active = true;
|
||||
}
|
||||
active: false
|
||||
sourceComponent: SysTrayMenu {
|
||||
Component.onCompleted: this.open();
|
||||
trayItemMenuHandle: root.item.menu
|
||||
trayItemId: root.item.id
|
||||
anchor {
|
||||
window: root.QsWindow.window
|
||||
item: root
|
||||
gravity: Config.options.bar.vertical
|
||||
? (Config.options.bar.bottom ? Edges.Left : Edges.Right)
|
||||
: (Config.options.bar.bottom ? Edges.Top : Edges.Bottom)
|
||||
edges: Config.options.bar.vertical
|
||||
? (Config.options.bar.bottom ? Edges.Left : Edges.Right)
|
||||
: (Config.options.bar.bottom ? Edges.Top : Edges.Bottom)
|
||||
}
|
||||
onMenuOpened: (window) => root.menuOpened(window);
|
||||
onMenuClosed: {
|
||||
root.menuClosed();
|
||||
menu.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
visible: !Config.options.tray.monochromeIcons
|
||||
source: root.item.icon
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: Config.options.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
|
||||
}
|
||||
|
||||
}
|
||||
262
surfaces/quickshell/ii-base/modules/ii/bar/SysTrayMenu.qml
Normal file
262
surfaces/quickshell/ii-base/modules/ii/bar/SysTrayMenu.qml
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
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 string trayItemId: ""
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RippleButton {
|
||||
id: pinEntry
|
||||
buttonRadius: popupBackground.radius - popupBackground.padding
|
||||
horizontalPadding: 12
|
||||
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: 36
|
||||
Layout.topMargin: 0
|
||||
Layout.bottomMargin: 0
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: root.trayItemId !== undefined && root.trayItemId.length > 0 && stackView.depth === 1
|
||||
releaseAction: () => TrayService.togglePin(root.trayItemId);
|
||||
|
||||
contentItem: RowLayout {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: pinEntry.horizontalPadding
|
||||
rightMargin: pinEntry.horizontalPadding
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
MaterialSymbol {
|
||||
iconSize: 18
|
||||
text: "push_pin"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: TrayService.isPinned(root.trayItemId) ? Translation.tr("Unpin") : Translation.tr("Pin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 1
|
||||
color: Appearance.colors.colSubtext
|
||||
Layout.topMargin: 4
|
||||
Layout.bottomMargin: 4
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
126
surfaces/quickshell/ii-base/modules/ii/bar/SysTrayMenuEntry.qml
Normal file
126
surfaces/quickshell/ii-base/modules/ii/bar/SysTrayMenuEntry.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
surfaces/quickshell/ii-base/modules/ii/bar/UtilButtons.qml
Normal file
158
surfaces/quickshell/ii-base/modules/ii/bar/UtilButtons.qml
Normal file
|
|
@ -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) {
|
||||
Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`])
|
||||
} else {
|
||||
Quickshell.execDetached(["bash", "-c", `${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 "airwave"
|
||||
case PowerProfile.Performance: return "local_fire_department"
|
||||
}
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
320
surfaces/quickshell/ii-base/modules/ii/bar/Workspaces.qml
Normal file
320
surfaces/quickshell/ii-base/modules/ii/bar/Workspaces.qml
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
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 effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
|
||||
|
||||
readonly property int workspacesShown: Config.options.bar.workspaces.shown
|
||||
readonly property int workspaceGroup: Math.floor((effectiveActiveWorkspaceId - 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: (effectiveActiveWorkspaceId - 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(`hl.dsp.focus({workspace = "r+1"})`);
|
||||
else if (event.angleDelta.y > 0)
|
||||
Hyprland.dispatch(`hl.dsp.focus({workspace = "r-1"})`);
|
||||
}
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.BackButton
|
||||
onPressed: (event) => {
|
||||
if (event.button === Qt.BackButton) {
|
||||
Hyprland.dispatch(`hl.dsp.workspace.toggle_special("special")`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 && root.effectiveActiveWorkspaceId === index))
|
||||
property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && root.effectiveActiveWorkspaceId === 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 && root.effectiveActiveWorkspaceId === 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(`hl.dsp.focus({ workspace = ${workspaceValue}})`)
|
||||
width: vertical ? undefined : root.workspaceButtonWidth
|
||||
height: vertical ? root.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: (root.effectiveActiveWorkspaceId == 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: (root.effectiveActiveWorkspaceId == 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,56 @@
|
|||
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
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: !Config.options.bar.tooltips.clickToShow
|
||||
|
||||
onPressed: {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
Weather.getData();
|
||||
Quickshell.execDetached(["notify-send",
|
||||
Translation.tr("Weather"),
|
||||
Translation.tr("Refreshing (manually triggered)")
|
||||
, "-a", "Shell"
|
||||
])
|
||||
mouse.accepted = false
|
||||
}
|
||||
}
|
||||
|
||||
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,115 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Footer: last refresh
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Translation.tr("Last refresh: %1").arg(Weather.data.lastRefresh)
|
||||
font {
|
||||
weight: Font.Medium
|
||||
pixelSize: Appearance.font.pixelSize.smaller
|
||||
}
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
409
surfaces/quickshell/ii-base/modules/ii/cheatsheet/Cheatsheet.qml
Normal file
409
surfaces/quickshell/ii-base/modules/ii/cheatsheet/Cheatsheet.qml
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.synchronizer
|
||||
import Qt5Compat.GraphicalEffects
|
||||
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;
|
||||
}
|
||||
property bool keyboardReady: false
|
||||
exclusiveZone: 0
|
||||
implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2
|
||||
implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2
|
||||
WlrLayershell.namespace: "quickshell:cheatsheet"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
//--> It dont take that sweet time to open Anymore!
|
||||
WlrLayershell.keyboardFocus: cheatsheetRoot.keyboardReady ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {
|
||||
item: cheatsheetBackground
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: lockWidthTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (cheatsheetBackground.lockedWidth === 0) {
|
||||
const contentWidth = Math.max(
|
||||
headerColumn.implicitWidth,
|
||||
keybindsPage.implicitWidth,
|
||||
periodicTablePage.implicitWidth,
|
||||
);
|
||||
cheatsheetBackground.lockedWidth = contentWidth + cheatsheetBackground.padding * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: keyboardFocusTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (cheatsheetRoot.visible) {
|
||||
cheatsheetRoot.keyboardReady = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
GlobalFocusGrab.addDismissable(cheatsheetRoot);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
GlobalFocusGrab.removeDismissable(cheatsheetRoot);
|
||||
}
|
||||
Connections {
|
||||
target: cheatsheetRoot
|
||||
function onVisibleChanged() {
|
||||
if (cheatsheetRoot.visible) {
|
||||
cheatsheetRoot.keyboardReady = false;
|
||||
lockWidthTimer.restart();
|
||||
keyboardFocusTimer.restart();
|
||||
} else {
|
||||
keyboardFocusTimer.stop();
|
||||
cheatsheetRoot.keyboardReady = false;
|
||||
cheatsheetBackground.lockedWidth = 0;
|
||||
cheatsheetBackground.collapseSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: GlobalFocusGrab
|
||||
function onDismissed() {
|
||||
cheatsheetRoot.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Background
|
||||
StyledRectangularShadow {
|
||||
target: cheatsheetBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: cheatsheetBackground
|
||||
anchors.centerIn: parent
|
||||
focus: cheatsheetRoot.visible
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.windowRounding
|
||||
property real padding: 20
|
||||
property real lockedWidth: 0
|
||||
property bool searchExpanded: false
|
||||
|
||||
function collapseSearch() {
|
||||
searchExpanded = false;
|
||||
searchField.focus = false;
|
||||
if (searchField.text !== "") {
|
||||
searchField.text = "";
|
||||
}
|
||||
cheatsheetBackground.forceActiveFocus();
|
||||
GlobalFocusGrab.addDismissable(cheatsheetRoot);
|
||||
}
|
||||
|
||||
function expandSearch() {
|
||||
GlobalFocusGrab.removeDismissable(cheatsheetRoot);
|
||||
searchExpanded = true;
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
implicitWidth: lockedWidth > 0 ? lockedWidth : cheatsheetColumnLayout.implicitWidth + padding * 2
|
||||
implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
event.accepted = true;
|
||||
if (searchField.text.length > 0 || searchExpanded) {
|
||||
clearSearchButton.clicked();
|
||||
} else {
|
||||
cheatsheetRoot.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchField.activeFocus && cheatsheetBackground.searchExpanded) {
|
||||
event.accepted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Backspace) {
|
||||
if (!cheatsheetBackground.searchExpanded) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (searchField.text.length > 0) {
|
||||
searchField.text = searchField.text.substring(0, searchField.text.length - 1);
|
||||
}
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.text.length > 0) {
|
||||
expandSearch();
|
||||
searchField.text += event.text;
|
||||
searchField.cursorPosition = searchField.text.length;
|
||||
searchField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
RippleButton { // Close button
|
||||
id: closeButton
|
||||
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
|
||||
|
||||
ColumnLayout {
|
||||
id: headerColumn
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 6
|
||||
|
||||
Toolbar {
|
||||
id: tabToolbar
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enableShadow: false
|
||||
ToolbarTabBar {
|
||||
id: tabBar
|
||||
tabButtonList: root.tabButtonList
|
||||
|
||||
Synchronizer on currentIndex {
|
||||
property alias source: swipeView.currentIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: Math.max(keybindsPage.implicitWidth, periodicTablePage.implicitWidth)
|
||||
implicitHeight: 30
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !cheatsheetBackground.searchExpanded
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: cheatsheetBackground.expandSearch()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: searchHintRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
visible: !cheatsheetBackground.searchExpanded
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: Appearance.colors.colSubtext
|
||||
text: "search"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
text: Translation.tr("Type or click here to search…")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: searchFieldContainer
|
||||
anchors.centerIn: parent
|
||||
width: Math.max(tabBar.implicitWidth, 220)
|
||||
height: 30
|
||||
visible: cheatsheetBackground.searchExpanded
|
||||
enabled: cheatsheetBackground.searchExpanded
|
||||
|
||||
ToolbarTextField {
|
||||
id: searchField
|
||||
anchors.fill: parent
|
||||
enabled: cheatsheetBackground.searchExpanded
|
||||
padding: 6
|
||||
rightPadding: 32
|
||||
placeholderText: ""
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
event.accepted = true;
|
||||
if (cheatsheetBackground.searchExpanded || text.length > 0) {
|
||||
clearSearchButton.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (text.length === 0 && cheatsheetBackground.searchExpanded) {
|
||||
cheatsheetBackground.collapseSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconToolbarButton {
|
||||
id: clearSearchButton
|
||||
anchors {
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
rightMargin: 2
|
||||
}
|
||||
implicitWidth: 26
|
||||
implicitHeight: 26
|
||||
text: "close"
|
||||
onClicked: cheatsheetBackground.collapseSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwipeView { // Content pages
|
||||
id: swipeView
|
||||
Layout.topMargin: 5
|
||||
spacing: 10
|
||||
currentIndex: Persistent.states.cheatsheet.tabIndex
|
||||
onCurrentIndexChanged: {
|
||||
Persistent.states.cheatsheet.tabIndex = currentIndex;
|
||||
}
|
||||
|
||||
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 {
|
||||
id: keybindsPage
|
||||
searchQuery: searchField.text
|
||||
}
|
||||
CheatsheetPeriodicTable {
|
||||
id: periodicTablePage
|
||||
searchQuery: searchField.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,78 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "cheatsheet_search.js" as CheatsheetSearch
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property string searchQuery: ""
|
||||
property bool noMatches: false
|
||||
property real padding: 4
|
||||
implicitWidth: QsWindow?.window?.screen.width * 0.7 ?? 0
|
||||
implicitHeight: QsWindow?.window?.screen.height * 0.7 ?? 0
|
||||
|
||||
onSearchQueryChanged: matchUpdateTimer.restart()
|
||||
|
||||
Timer {
|
||||
id: matchUpdateTimer
|
||||
interval: 0
|
||||
onTriggered: {
|
||||
if (!root.searchQuery.trim()) {
|
||||
root.noMatches = false;
|
||||
return;
|
||||
}
|
||||
let any = false;
|
||||
for (let i = 0; i < flow.children.length; i++) {
|
||||
if (flow.children[i].visible) {
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
root.noMatches = !any;
|
||||
}
|
||||
}
|
||||
|
||||
StyledFlickable {
|
||||
id: flickable
|
||||
clip: true
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.rounding.small
|
||||
contentHeight: height
|
||||
contentWidth: flow.implicitWidth
|
||||
|
||||
Flow {
|
||||
id: flow
|
||||
height: flickable.height
|
||||
flow: Flow.TopToBottom
|
||||
spacing: 10
|
||||
Repeater {
|
||||
model: [...HyprlandKeybinds.keybindCategories, ""]
|
||||
delegate: CheatsheetKeybindsCategory {
|
||||
required property var modelData
|
||||
categoryName: modelData
|
||||
searchQuery: root.searchQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollEdgeFade {
|
||||
target: flickable
|
||||
vertical: false
|
||||
color: Appearance.colors.colLayer0Base
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: root.noMatches
|
||||
font.pixelSize: Appearance.font.pixelSize.large
|
||||
color: Appearance.colors.colSubtext
|
||||
text: Translation.tr("No matching keybinds")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "cheatsheet_search.js" as CheatsheetSearch
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
// Notes:
|
||||
// We deal with keybinds being numbered 1, 2, etc by discarding 2+, keeping 1 and replacing it with a generic "<Number>"
|
||||
Column {
|
||||
id: root
|
||||
required property string categoryName
|
||||
property string searchQuery: ""
|
||||
readonly property bool isCategorized: categoryName?.length > 0
|
||||
property int maxBindWidth: 0
|
||||
property real columnSpacing: 40
|
||||
property real titleSpacing: 7
|
||||
|
||||
// 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", "SUPER_R"]
|
||||
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 : {},
|
||||
)
|
||||
|
||||
function modMaskToStringList(modMask: int): list<string> {
|
||||
var list = [];
|
||||
// Funny mathematical order but we wanna have this natural user-facing order
|
||||
if (modMask & (1 << 2)) { list.push("Ctrl"); }
|
||||
if (modMask & (1 << 6)) { list.push("Super"); }
|
||||
if (modMask & (1 << 0)) { list.push("Shift"); }
|
||||
if (modMask & (1 << 3)) { list.push("Alt"); }
|
||||
if (modMask & (1 << 1)) { list.push("Caps"); }
|
||||
if (modMask & (1 << 4)) { list.push("Mod2"); }
|
||||
if (modMask & (1 << 5)) { list.push("Mod3"); }
|
||||
if (modMask & (1 << 7)) { list.push("Mod5"); }
|
||||
return list;
|
||||
}
|
||||
|
||||
visible: repeater.model.length > 0
|
||||
spacing: titleSpacing
|
||||
|
||||
StyledText {
|
||||
text: root.isCategorized ? root.categoryName : "Uncategorized"
|
||||
font.pixelSize: Appearance.font.pixelSize.title
|
||||
}
|
||||
|
||||
function hasDescription(bind) {
|
||||
return bind.description?.length > 0;
|
||||
}
|
||||
|
||||
function isCategory(bind, categoryName) {
|
||||
return bind.description.substring(0, bind.description.indexOf(":")) === categoryName;
|
||||
}
|
||||
|
||||
function isUncategorized(bind) {
|
||||
return bind.description.indexOf(":") === -1;
|
||||
}
|
||||
|
||||
function containsNonFirstRepetitive(bind) {
|
||||
const key = bind.key;
|
||||
if (key.includes("mouse") || key.includes("page")) return false;
|
||||
// Contains non-1 number
|
||||
if (/\d/.test(key) && !key.includes("1")) return true;
|
||||
// Contains non-left direction
|
||||
if (/^(right|up|down)\b/i.test(key)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function containsFirstRepetitive(bind) {
|
||||
const key = bind.key;
|
||||
return key.includes("1") || /left/i.test(key);
|
||||
}
|
||||
|
||||
function transformKey(key) {
|
||||
const replaced = root.keySubstitutions[key] || key;
|
||||
const denumbered = replaced.replace("1", "<Number>");
|
||||
const dedirectioned = denumbered.replace("Left", "<Direction>");
|
||||
return dedirectioned;
|
||||
}
|
||||
|
||||
function transformDescription(bind, categoryName) {
|
||||
const description = bind.description
|
||||
const regex = new RegExp("\\s*" + categoryName + "\\s*:\\s*");
|
||||
const decategorized = description.replace(regex, "");
|
||||
if (!containsFirstRepetitive(bind)) return decategorized;
|
||||
const denumbered = decategorized.replace("1", "<Number>");
|
||||
const dedirectioned = denumbered.replace(/ \b(left|right|up|down)\b/i, " <Direction>");
|
||||
return dedirectioned;
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: {
|
||||
let binds;
|
||||
if (!root.isCategorized) {
|
||||
binds = HyprlandKeybinds.keybinds.filter(bind => root.hasDescription(bind) && root.isUncategorized(bind) && !root.containsNonFirstRepetitive(bind));
|
||||
} else {
|
||||
binds = HyprlandKeybinds.keybinds.filter(bind => root.hasDescription(bind) && root.isCategory(bind, root.categoryName) && !root.containsNonFirstRepetitive(bind));
|
||||
}
|
||||
if (root.searchQuery.trim().length === 0) return binds;
|
||||
const category = root.isCategorized ? root.categoryName : "";
|
||||
return binds.filter(bind => CheatsheetSearch.matchesQuery(
|
||||
CheatsheetSearch.keybindHaystack(bind, root.keySubstitutions, category),
|
||||
root.searchQuery,
|
||||
));
|
||||
}
|
||||
delegate: BindLine {
|
||||
required property var modelData
|
||||
keyData: modelData
|
||||
categoryName: root.categoryName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BindLine: Row {
|
||||
id: bindLine
|
||||
required property var keyData
|
||||
property string categoryName: ""
|
||||
|
||||
Row {
|
||||
spacing: 16
|
||||
Row {
|
||||
id: modRow
|
||||
Component.onCompleted: root.maxBindWidth = Math.max(root.maxBindWidth, implicitWidth)
|
||||
width: root.maxBindWidth
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: {
|
||||
const modList = root.modMaskToStringList(bindLine.keyData.modmask).map(mod => root.keySubstitutions[mod] || mod)
|
||||
if (modList.length == 0) return []
|
||||
if (Config.options.cheatsheet.splitButtons) return modList;
|
||||
return [modList.join(" ")]
|
||||
}
|
||||
delegate: KeyboardKey {
|
||||
required property var modelData
|
||||
key: root.transformKey(modelData)
|
||||
pixelSize: Config.options.cheatsheet.fontSize.key
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: keybindPlus
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !keyBlacklist.includes(bindLine.keyData.key) && bindLine.keyData.modmask > 0
|
||||
text: "+"
|
||||
}
|
||||
KeyboardKey {
|
||||
id: keybindKey
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !keyBlacklist.includes(bindLine.keyData.key)
|
||||
key: root.transformKey(bindLine.keyData.key)
|
||||
pixelSize: Config.options.cheatsheet.fontSize.key
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
}
|
||||
Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitWidth: commentText.implicitWidth + root.columnSpacing
|
||||
implicitHeight: commentText.implicitHeight
|
||||
StyledText {
|
||||
id: commentText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
font.pixelSize: Config.options.cheatsheet.fontSize.comment || Appearance.font.pixelSize.smaller
|
||||
text: root.transformDescription(bindLine.keyData, bindLine.categoryName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import "periodic_table.js" as PTable
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property string searchQuery: ""
|
||||
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
|
||||
searchQuery: root.searchQuery
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
searchQuery: root.searchQuery
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import "cheatsheet_search.js" as CheatsheetSearch
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
required property var element
|
||||
property string searchQuery: ""
|
||||
|
||||
readonly property bool dimmed: searchQuery.trim().length > 0
|
||||
&& element.type !== "empty"
|
||||
&& !CheatsheetSearch.matchesElement(element, searchQuery)
|
||||
|
||||
opacity: element.type != "empty" ? (dimmed ? 0.28 : 1) : 0
|
||||
enabled: element.type === "empty" || !dimmed
|
||||
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,88 @@
|
|||
.pragma library
|
||||
|
||||
function queryTokens(query) {
|
||||
if (!query) return [];
|
||||
return query.trim().toLowerCase().split(/\s+/).filter(token => token.length > 0);
|
||||
}
|
||||
|
||||
function matchesQuery(haystack, query) {
|
||||
const tokens = queryTokens(query);
|
||||
if (tokens.length === 0) return true;
|
||||
return tokens.every(token => String(haystack).toLowerCase().includes(token));
|
||||
}
|
||||
|
||||
function lookupSubstitution(substitutions, key) {
|
||||
if (!key) return "";
|
||||
if (substitutions[key]) return substitutions[key];
|
||||
const title = key.charAt(0).toUpperCase() + key.slice(1).toLowerCase();
|
||||
if (substitutions[title]) return substitutions[title];
|
||||
if (substitutions[key.toUpperCase()]) return substitutions[key.toUpperCase()];
|
||||
if (substitutions[key.toLowerCase()]) return substitutions[key.toLowerCase()];
|
||||
return key;
|
||||
}
|
||||
|
||||
function modMaskToStringList(modMask) {
|
||||
const list = [];
|
||||
if (modMask & (1 << 2)) list.push("Ctrl");
|
||||
if (modMask & (1 << 6)) list.push("Super");
|
||||
if (modMask & (1 << 0)) list.push("Shift");
|
||||
if (modMask & (1 << 3)) list.push("Alt");
|
||||
if (modMask & (1 << 1)) list.push("Caps");
|
||||
if (modMask & (1 << 4)) list.push("Mod2");
|
||||
if (modMask & (1 << 5)) list.push("Mod3");
|
||||
if (modMask & (1 << 7)) list.push("Mod5");
|
||||
return list;
|
||||
}
|
||||
|
||||
function containsFirstRepetitive(key) {
|
||||
return key.includes("1") || /left/i.test(key);
|
||||
}
|
||||
|
||||
function transformKey(key, substitutions) {
|
||||
const replaced = lookupSubstitution(substitutions, key);
|
||||
return replaced.replace("1", "<Number>").replace("Left", "<Direction>");
|
||||
}
|
||||
|
||||
function transformDescription(bind, categoryName) {
|
||||
const description = bind.description || "";
|
||||
const decategorized = categoryName
|
||||
? description.replace(new RegExp("\\s*" + categoryName + "\\s*:\\s*"), "")
|
||||
: description;
|
||||
const key = bind.key || "";
|
||||
if (!containsFirstRepetitive(key)) return decategorized;
|
||||
const denumbered = decategorized.replace("1", "<Number>");
|
||||
return denumbered.replace(/ \b(left|right|up|down)\b/i, " <Direction>");
|
||||
}
|
||||
|
||||
function mouseKeySearchTerms(rawKey) {
|
||||
switch (rawKey) {
|
||||
case "mouse:272": return "LMB left mouse";
|
||||
case "mouse:273": return "RMB right mouse";
|
||||
case "mouse:275": return "MouseBack mouse back";
|
||||
case "mouse_up": return "Scroll Down scroll down mouse up";
|
||||
case "mouse_down": return "Scroll Up scroll up mouse down";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
function keybindHaystack(bind, substitutions, categoryName) {
|
||||
const modParts = [];
|
||||
for (const mod of modMaskToStringList(bind.modmask)) {
|
||||
modParts.push(mod);
|
||||
modParts.push(lookupSubstitution(substitutions, mod));
|
||||
}
|
||||
const rawKey = bind.key || "";
|
||||
const displayKey = transformKey(rawKey, substitutions);
|
||||
const description = bind.description || "";
|
||||
const displayDescription = transformDescription(bind, categoryName);
|
||||
const categoryPrefix = categoryName || description.substring(0, description.indexOf(":"));
|
||||
const keyTerms = [rawKey, displayKey, mouseKeySearchTerms(rawKey)].join(" ");
|
||||
return [modParts.join(" "), keyTerms, description, displayDescription, categoryPrefix].join(" ");
|
||||
}
|
||||
|
||||
function matchesElement(element, query) {
|
||||
if (!element || element.type === "empty") return false;
|
||||
if (queryTokens(query).length === 0) return true;
|
||||
const haystack = [element.name, element.symbol, String(element.weight)].join(" ");
|
||||
return matchesQuery(haystack, query);
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
148
surfaces/quickshell/ii-base/modules/ii/dock/Dock.qml
Normal file
148
surfaces/quickshell/ii-base/modules/ii/dock/Dock.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
surfaces/quickshell/ii-base/modules/ii/dock/DockAppButton.qml
Normal file
140
surfaces/quickshell/ii-base/modules/ii/dock/DockAppButton.qml
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
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"
|
||||
property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId)
|
||||
enabled: !isSeparator
|
||||
implicitWidth: isSeparator ? 1 : implicitHeight - topInset - bottomInset
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
|
||||
function onApplicationsChanged() {
|
||||
root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel.appId);
|
||||
}
|
||||
}
|
||||
|
||||
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: () => {
|
||||
TaskbarApps.togglePin(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
228
surfaces/quickshell/ii-base/modules/ii/dock/DockApps.qml
Normal file
228
surfaces/quickshell/ii-base/modules/ii/dock/DockApps.qml
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
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: null
|
||||
property bool buttonHovered: false
|
||||
property bool requestDockShow: previewPopup.show
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Appearance.sizes.hyprlandGapsOut
|
||||
implicitWidth: listView.implicitWidth
|
||||
|
||||
function popupCenterXForButton(button) {
|
||||
if (!button || !root.QsWindow)
|
||||
return 0;
|
||||
return root.QsWindow.mapFromItem(button, button.width / 2, 0).x;
|
||||
}
|
||||
|
||||
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 shouldShow: (popupMouseArea.containsMouse || root.buttonHovered) && appTopLevel && appTopLevel.toplevels && appTopLevel.toplevels.length > 0
|
||||
|
||||
property bool show: false
|
||||
property real cachedCenterX: 0
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onLastHoveredButtonChanged() {
|
||||
if (root.lastHoveredButton && root.QsWindow)
|
||||
previewPopup.cachedCenterX = root.popupCenterXForButton(root.lastHoveredButton);
|
||||
}
|
||||
function onButtonHoveredChanged() {
|
||||
if (root.buttonHovered && root.lastHoveredButton && root.QsWindow)
|
||||
previewPopup.cachedCenterX = root.popupCenterXForButton(root.lastHoveredButton);
|
||||
updateTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onShouldShowChanged: {
|
||||
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.opacity > 0
|
||||
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: previewPopup.cachedCenterX - 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.m3colors.m3surfaceContainer
|
||||
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
|
||||
Layout.fillHeight: true
|
||||
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
|
||||
StyledText {
|
||||
Layout.margins: 5
|
||||
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: root.windowControlsHeight
|
||||
baseHeight: root.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
implicitHeight: screencopyView.height
|
||||
implicitWidth: screencopyView.width
|
||||
ScreencopyView {
|
||||
id: screencopyView
|
||||
anchors.centerIn: parent
|
||||
captureSource: windowButton.modelData
|
||||
live: true
|
||||
paintCursor: true
|
||||
constraintSize: Qt.size(root.maxWindowPreviewWidth, root.maxWindowPreviewHeight)
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: screencopyView.width
|
||||
height: screencopyView.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
surfaces/quickshell/ii-base/modules/ii/dock/DockButton.qml
Normal file
13
surfaces/quickshell/ii-base/modules/ii/dock/DockButton.qml
Normal file
|
|
@ -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
|
||||
}
|
||||
77
surfaces/quickshell/ii-base/modules/ii/lock/Lock.qml
Normal file
77
surfaces/quickshell/ii-base/modules/ii/lock/Lock.qml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.panels.lock
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
LockScreen {
|
||||
id: root
|
||||
|
||||
// Monitor name -> workspace id to restore on unlock (set when locking)
|
||||
property var savedWorkspaces: ({})
|
||||
|
||||
Timer {
|
||||
id: restoreTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
var batch = ""
|
||||
for (var j = 0; j < Quickshell.screens.length; ++j) {
|
||||
var monName = Quickshell.screens[j].name
|
||||
var wsId = root.savedWorkspaces[monName]
|
||||
if (wsId !== undefined) {
|
||||
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${monName}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${wsId}})';`
|
||||
}
|
||||
}
|
||||
if (batch.length > 0) {
|
||||
Quickshell.execDetached(["bash", "-c", batch])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lockSurface: LockSurface {
|
||||
context: root.context
|
||||
}
|
||||
|
||||
// Single batch for lock and unlock so we don't race multiple hyprctl calls
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenLockedChanged() {
|
||||
if (GlobalStates.screenLocked) {
|
||||
// Lock: save workspace per monitor and move all to temp workspace in one batch
|
||||
var next = {}
|
||||
var batch = "keyword animation workspaces,1,7,menu_decel,slidevert; "
|
||||
for (var i = 0; i < Quickshell.screens.length; ++i) {
|
||||
var mon = Quickshell.screens[i].name
|
||||
var mData = HyprlandData.monitors.find(m => m.name === mon)
|
||||
if (mData?.activeWorkspace == undefined) {
|
||||
return;
|
||||
}
|
||||
var ws = (mData?.activeWorkspace?.id ?? 1)
|
||||
next[mon] = ws
|
||||
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${mon}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${2147483647 - ws}})';`
|
||||
}
|
||||
root.savedWorkspaces = next
|
||||
Quickshell.execDetached(["bash", "-c", batch])
|
||||
} else {
|
||||
restoreTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push everything down (visual only; workspace switch is in Connections above)
|
||||
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 ?? 0
|
||||
property int horizontalSqueeze: (modelData?.width ?? 0) * 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
500
surfaces/quickshell/ii-base/modules/ii/lock/LockSurface.qml
Normal file
500
surfaces/quickshell/ii-base/modules/ii/lock/LockSurface.qml
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
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.common.panels.lock
|
||||
import qs.modules.ii.bar as Bar
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.modules.ii.mediaControls
|
||||
|
||||
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
|
||||
|
||||
// Whether a player with a title is currently active
|
||||
readonly property bool mediaPlayerAvailable: MprisController.activePlayer !== null && MprisController.activePlayer.trackTitle
|
||||
|
||||
// Gate that keeps the Loader alive during exit animation.
|
||||
// Set to true when player appears, set to false only after exit anim finishes.
|
||||
property bool mediaLoaderActive: false
|
||||
|
||||
onMediaPlayerAvailableChanged: {
|
||||
if (mediaPlayerAvailable) {
|
||||
// Player appeared: activate loader (entry anim fires via onLoaded)
|
||||
mediaLoaderActive = true
|
||||
} else {
|
||||
// Player disappeared: run exit anim, deactivate loader when done
|
||||
if (lockscreenMediaController.item) {
|
||||
entryAnim.stop()
|
||||
mediaExitAnim.restart()
|
||||
} else {
|
||||
mediaLoaderActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visualizer data for lockscreen media controls
|
||||
property list<real> visualizerPoints: []
|
||||
|
||||
Process {
|
||||
id: cavaProc
|
||||
running: root.mediaLoaderActive && root.mediaPlayerAvailable
|
||||
onRunningChanged: {
|
||||
if (!cavaProc.running)
|
||||
root.visualizerPoints = [];
|
||||
}
|
||||
command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
|
||||
root.visualizerPoints = points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
property bool ctrlHeld: false
|
||||
Keys.onPressed: event => {
|
||||
root.context.resetClearTimer();
|
||||
if (event.key === Qt.Key_Control) {
|
||||
root.ctrlHeld = true;
|
||||
}
|
||||
if (event.key === Qt.Key_Escape) { // Esc to clear
|
||||
root.context.currentText = "";
|
||||
}
|
||||
forceFieldFocus();
|
||||
}
|
||||
Keys.onReleased: event => {
|
||||
if (event.key === Qt.Key_Control) {
|
||||
root.ctrlHeld = false;
|
||||
}
|
||||
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 ]]"
|
||||
// }
|
||||
// }
|
||||
|
||||
Loader {
|
||||
id: lockscreenMediaController
|
||||
|
||||
// Controlled manually via mediaLoaderActive so exit anim can finish
|
||||
// before the item is destroyed.
|
||||
active: root.mediaLoaderActive
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: mainIsland.top
|
||||
bottomMargin: 15
|
||||
}
|
||||
|
||||
// Driven imperatively — no Behavior, so start values apply instantly.
|
||||
property real mediaScale: 0.85
|
||||
property real mediaOpacity: 0.0
|
||||
|
||||
scale: mediaScale * root.toolbarScale
|
||||
opacity: mediaOpacity * root.toolbarOpacity
|
||||
|
||||
// Entry: fires after sourceComponent is fully instantiated.
|
||||
onLoaded: {
|
||||
mediaScale = 0.85
|
||||
mediaOpacity = 0.0
|
||||
entryAnim.restart()
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: entryAnim
|
||||
NumberAnimation {
|
||||
target: lockscreenMediaController
|
||||
property: "mediaScale"
|
||||
to: 1.0
|
||||
duration: Appearance.animation.elementMove.duration * 1.2
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
NumberAnimation {
|
||||
target: lockscreenMediaController
|
||||
property: "mediaOpacity"
|
||||
to: 1.0
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Exit: deactivates loader when animation finishes.
|
||||
SequentialAnimation {
|
||||
id: mediaExitAnim
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: lockscreenMediaController
|
||||
property: "mediaScale"
|
||||
to: 0.85
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Easing.InBack
|
||||
easing.overshoot: 1.2
|
||||
}
|
||||
NumberAnimation {
|
||||
target: lockscreenMediaController
|
||||
property: "mediaOpacity"
|
||||
to: 0.0
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
}
|
||||
// Only destroy the item after the animation has fully completed.
|
||||
ScriptAction {
|
||||
script: root.mediaLoaderActive = false
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: PlayerControl {
|
||||
player: MprisController.activePlayer
|
||||
visualizerPoints: root.visualizerPoints
|
||||
implicitWidth: Appearance.sizes.mediaControlsWidth
|
||||
implicitHeight: Appearance.sizes.mediaControlsHeight
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
selectedTextColor: materialShapeChars ? "transparent" : Appearance.colors.colOnSecondaryContainer
|
||||
selectionColor: materialShapeChars ? "transparent" : Appearance.colors.colSecondaryContainer
|
||||
|
||||
// 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(ctrlHeld);
|
||||
}
|
||||
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
|
||||
ErrorShakeAnimation {
|
||||
id: wrongPasswordShakeAnim
|
||||
target: passwordBox
|
||||
}
|
||||
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
|
||||
selectionStart: passwordBox.selectionStart
|
||||
selectionEnd: passwordBox.selectionEnd
|
||||
cursorPosition: passwordBox.cursorPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 root.ctrlHeld ? "coffee" : "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
|
||||
}
|
||||
}
|
||||
}
|
||||
128
surfaces/quickshell/ii-base/modules/ii/lock/PasswordChars.qml
Normal file
128
surfaces/quickshell/ii-base/modules/ii/lock/PasswordChars.qml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
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
|
||||
property int selectionStart
|
||||
property int selectionEnd
|
||||
property int cursorPosition
|
||||
|
||||
property color color: Appearance.colors.colPrimary
|
||||
property color selectedTextColor: Appearance.colors.colOnSecondaryContainer
|
||||
property color selectionColor: Appearance.colors.colSecondaryContainer
|
||||
|
||||
property int charSize: 20
|
||||
|
||||
contentWidth: dotsRow.implicitWidth
|
||||
contentX: (Math.max(contentWidth - width, 0))
|
||||
Behavior on contentX {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: cursor
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: root.charSize * root.cursorPosition
|
||||
}
|
||||
color: root.color
|
||||
implicitWidth: 2
|
||||
implicitHeight: root.charSize
|
||||
Behavior on anchors.leftMargin {
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(cursor)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: dotsRow
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: 4 - 5 // -5 to account for spacing being simulated by char item width
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel { // TODO: use proper custom object model to insert new char at the correct pos
|
||||
values: Array(root.length)
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: charItem
|
||||
required property int index
|
||||
implicitWidth: root.charSize
|
||||
implicitHeight: root.charSize
|
||||
property bool selected: index >= root.selectionStart && index < root.selectionEnd
|
||||
|
||||
color: ColorUtils.transparentize(root.selectionColor, selected ? 0 : 1)
|
||||
|
||||
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: charItem.selected ? root.selectedTextColor : root.color
|
||||
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,294 @@
|
|||
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: MprisController.players
|
||||
// Hide ghost players: Stopped AND no title. Chromium (and others) leave
|
||||
// an MPRIS registration on D-Bus after a tab closes — still owns the
|
||||
// bus name, playbackState=Stopped, no title/artist, sometimes a stale
|
||||
// mpris:artUrl. Filtering on Stopped alone is too aggressive: real
|
||||
// players (e.g. pear-desktop / youtube-music) transition through
|
||||
// Stopped during track changes while keeping the previous trackTitle
|
||||
// set, and filtering them produces a brief "No active player"
|
||||
// placeholder flash mid-skip.
|
||||
readonly property var _rawMeaningfulPlayers: filterDuplicatePlayers(
|
||||
realPlayers.filter(p => !(p?.playbackState === MprisPlaybackState.Stopped && !p?.trackTitle))
|
||||
)
|
||||
|
||||
// Stabilized view: holds the previous non-empty list for a brief grace
|
||||
// window when the raw list goes empty, so we don't destroy/recreate
|
||||
// PlayerControl delegates (and shrink/grow the panel) during normal
|
||||
// track changes. Real "all players gone" states get through after the
|
||||
// timer fires.
|
||||
property var meaningfulPlayers: _rawMeaningfulPlayers
|
||||
property bool noActivePlayers: false
|
||||
on_RawMeaningfulPlayersChanged: {
|
||||
if (_rawMeaningfulPlayers.length > 0) {
|
||||
meaningfulPlayers = _rawMeaningfulPlayers;
|
||||
noActivePlayers = false;
|
||||
noActivePlayerDelay.stop();
|
||||
} else {
|
||||
noActivePlayerDelay.restart();
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: noActivePlayerDelay
|
||||
interval: 800
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (root._rawMeaningfulPlayers.length === 0) {
|
||||
root.meaningfulPlayers = [];
|
||||
root.noActivePlayers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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: []
|
||||
|
||||
function filterDuplicatePlayers(players) {
|
||||
let filtered = [];
|
||||
let used = new Set();
|
||||
|
||||
// Higher rank wins when picking the canonical player from a duplicate
|
||||
// group. A stale Stopped player with a non-empty artUrl should never
|
||||
// mask a live Playing one (Chromium leaves behind such ghosts).
|
||||
const playRank = p => (p?.playbackState === MprisPlaybackState.Playing) ? 2
|
||||
: (p?.playbackState === MprisPlaybackState.Paused) ? 1 : 0;
|
||||
const hasArt = p => !!(p?.trackArtUrl && p.trackArtUrl.length > 0);
|
||||
|
||||
for (let i = 0; i < players.length; ++i) {
|
||||
if (used.has(i))
|
||||
continue;
|
||||
let p1 = players[i];
|
||||
let group = [i];
|
||||
|
||||
// Find duplicates: matching titles, OR same playback state plus
|
||||
// near-identical position/length (plasma-browser-integration vs
|
||||
// the native browser bus). Use absolute differences — the
|
||||
// original `<= 2` on raw subtraction is true for any negative
|
||||
// diff, so a Stopped player at position=0/length=0 incorrectly
|
||||
// matches every Playing player.
|
||||
for (let j = i + 1; j < players.length; ++j) {
|
||||
let p2 = players[j];
|
||||
const titleMatch = p1.trackTitle && p2.trackTitle &&
|
||||
(p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle));
|
||||
const motionMatch = p1.playbackState === p2.playbackState &&
|
||||
Math.abs(p1.position - p2.position) <= 2 &&
|
||||
Math.abs(p1.length - p2.length) <= 2;
|
||||
if (titleMatch || motionMatch) {
|
||||
group.push(j);
|
||||
}
|
||||
}
|
||||
|
||||
// Tiebreaker: highest play-state rank, then non-empty trackArtUrl,
|
||||
// then first encountered.
|
||||
let chosenIdx = group[0];
|
||||
for (let k = 1; k < group.length; ++k) {
|
||||
const cand = players[group[k]];
|
||||
const best = players[chosenIdx];
|
||||
if (playRank(cand) > playRank(best)) chosenIdx = group[k];
|
||||
else if (playRank(cand) === playRank(best) && hasArt(cand) && !hasArt(best)) chosenIdx = group[k];
|
||||
}
|
||||
|
||||
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 && root.realPlayers.length === 0) {
|
||||
GlobalStates.mediaControlsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: panelWindow
|
||||
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 ? ((panelWindow.screen.height / 2) - widgetHeight * 1.5) : Appearance.sizes.barHeight
|
||||
bottom: Appearance.sizes.barHeight
|
||||
left: Config.options.bar.vertical ? Appearance.sizes.barHeight : ((panelWindow.screen.width / 2) - (osdWidth / 2) - widgetWidth)
|
||||
right: Appearance.sizes.barHeight
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: playerColumnLayout
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
GlobalFocusGrab.addDismissable(panelWindow);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
GlobalFocusGrab.removeDismissable(panelWindow);
|
||||
}
|
||||
Connections {
|
||||
target: GlobalFocusGrab
|
||||
function onDismissed() {
|
||||
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 (panelWindow.anchors.left)
|
||||
return Qt.AlignLeft;
|
||||
if (panelWindow.anchors.right)
|
||||
return Qt.AlignRight;
|
||||
return Qt.AlignHCenter;
|
||||
}
|
||||
Layout.leftMargin: Appearance.sizes.hyprlandGapsOut
|
||||
Layout.rightMargin: Appearance.sizes.hyprlandGapsOut
|
||||
visible: root.noActivePlayers
|
||||
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,318 @@
|
|||
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 color artDominantColor: ColorUtils.mix((colorQuantizer?.colors[0] ?? Appearance.colors.colPrimary), Appearance.colors.colPrimaryContainer, 0.8) || Appearance.m3colors.m3secondaryContainer
|
||||
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
|
||||
|
||||
// Cover art is downloaded centrally by MprisController (per-player Process
|
||||
// that runs regardless of panel state), and tracked there as the
|
||||
// highest-quality variant seen for each (player, track) pair. We just
|
||||
// read that cache. This survives both:
|
||||
// - panel close/reopen (we weren't here to receive emissions; the
|
||||
// central downloader was)
|
||||
// - players that emit multiple sizes per track (Firefox: 544x544 then
|
||||
// 60x60 thumbnail; Chromium: several near-identical tmp files) —
|
||||
// bestArtByPlayer keeps the largest.
|
||||
// Cache the last non-empty trackTitle/trackArtist. Some players
|
||||
// (pear-desktop / youtube-music) clear metadata briefly during a track
|
||||
// change before emitting the new track's values; binding the UI
|
||||
// directly to player.trackTitle would make the title flash to
|
||||
// "Untitled" mid-skip and would also invalidate the trackKey, dropping
|
||||
// the cover image.
|
||||
property string _stableTitle: ""
|
||||
property string _stableArtist: ""
|
||||
Connections {
|
||||
target: root.player
|
||||
function onTrackTitleChanged() {
|
||||
if (root.player?.trackTitle) root._stableTitle = root.player.trackTitle;
|
||||
}
|
||||
function onTrackArtistChanged() {
|
||||
if (root.player?.trackArtist) root._stableArtist = root.player.trackArtist;
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (root.player?.trackTitle) _stableTitle = root.player.trackTitle;
|
||||
if (root.player?.trackArtist) _stableArtist = root.player.trackArtist;
|
||||
}
|
||||
|
||||
// title|artist only; see MprisController._trackKeyOf for why album is omitted.
|
||||
readonly property string _trackKey: `${_stableTitle}|${_stableArtist}`
|
||||
readonly property string displayedArtFilePath: {
|
||||
const entry = MprisController.getBestArt(player, _trackKey);
|
||||
return entry ? Qt.resolvedUrl(entry.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()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
StyledImage {
|
||||
id: blurredArt
|
||||
anchors.fill: parent
|
||||
source: root.displayedArtFilePath
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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._stableTitle) || "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._stableArtist
|
||||
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 => Config.options.notifications.forceMonitor.enable ? s.name === Config.options.notifications.forceMonitor.name : 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,237 @@
|
|||
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"
|
||||
},
|
||||
{
|
||||
id: "gamma",
|
||||
sourceUrl: "indicators/GammaIndicator.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 {
|
||||
target: Hyprsunset
|
||||
function onGammaChangeAttempt() {
|
||||
root.protectionMessage = "";
|
||||
root.currentIndicator = "gamma";
|
||||
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,106 @@
|
|||
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 alias from: valueProgressBar.from
|
||||
property alias to: valueProgressBar.to
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.temperatureActive ? "routine" : "light_mode"
|
||||
rotateIcon: true
|
||||
scaleIcon: true
|
||||
name: Translation.tr("Brightness")
|
||||
value: root.brightnessMonitor?.brightness ?? 50
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import qs.modules.ii.onScreenDisplay
|
||||
|
||||
OsdValueIndicator {
|
||||
id: rotateIcon
|
||||
|
||||
icon: "wb_twilight"
|
||||
name: Translation.tr("Gamma")
|
||||
from: Hyprsunset.gammaLowerLimit / 100
|
||||
value: Hyprsunset.gamma / 100 ?? 0.5
|
||||
}
|
||||
|
|
@ -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,174 @@
|
|||
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: {
|
||||
PhysicalKeyboard.active = oskLoader.active;
|
||||
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
|
||||
}
|
||||
|
||||
// Make it usable with other panels
|
||||
Component.onCompleted: {
|
||||
GlobalFocusGrab.addPersistent(oskRoot);
|
||||
}
|
||||
Component.onDestruction: {
|
||||
GlobalFocusGrab.removePersistent(oskRoot);
|
||||
}
|
||||
|
||||
// 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,137 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visual feedback for physical keyboard presses
|
||||
Connections {
|
||||
target: PhysicalKeyboard
|
||||
enabled: root.enabled && root.keycode !== undefined
|
||||
function onKeyPressed(keycode) {
|
||||
if (keycode === root.keycode) {
|
||||
root.startRipple(root.width / 2, root.height / 2);
|
||||
}
|
||||
}
|
||||
function onKeyReleased(keycode) {
|
||||
if (keycode === root.keycode) {
|
||||
root.fadeRipple();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,594 @@
|
|||
// 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 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Greek (polytonic)": {
|
||||
name_short: "EL",
|
||||
description: "Polytonic Greek - Full",
|
||||
comment: "Greek keyboard layout with polytonic diacritical marks (XKB gr(polytonic))",
|
||||
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: "῀", labelAlt: "`", shape: "normal", keycode: 41 }, // dead perispomeni / AltGr: grave
|
||||
{ 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: "῀", labelAlt: ";", shape: "normal", keycode: 16 }, // q → dead perispomeni / AltGr: ; (Greek question mark)
|
||||
{ keytype: "normal", label: "ς", labelShift: "Σ", labelAlt: "¨", shape: "normal", keycode: 17 }, // w → final sigma / AltGr: dead diaeresis
|
||||
{ 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: "{", labelAlt: "«", shape: "normal", keycode: 26 },
|
||||
{ keytype: "normal", label: "]", labelShift: "}", labelAlt: "»", 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: "¨", labelAlt: "ͺ", shape: "normal", keycode: 39 }, // dead oxia / dead dialytika / AltGr: iota subscript
|
||||
{ keytype: "normal", label: "`", labelShift: "`", labelAlt: "'", shape: "normal", keycode: 40 }, // dead varia / AltGr: apostrophe
|
||||
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", 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", labelShift: "Shift", labelCaps: "Caps", 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 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Arabic": {
|
||||
name_short: "AR",
|
||||
description: "Arabic Standard - Full",
|
||||
comment: "Standard Arabic keyboard layout (XKB ara)",
|
||||
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 }, // Thal / Shadda
|
||||
{ keytype: "normal", label: "١", labelShift: "!", labelAlt: "1", shape: "normal", keycode: 2 },
|
||||
{ keytype: "normal", label: "٢", labelShift: "@", labelAlt: "2", shape: "normal", keycode: 3 },
|
||||
{ keytype: "normal", label: "٣", labelShift: "#", labelAlt: "3", shape: "normal", keycode: 4 },
|
||||
{ keytype: "normal", label: "٤", labelShift: "$", labelAlt: "4", shape: "normal", keycode: 5 },
|
||||
{ keytype: "normal", label: "٥", labelShift: "%", labelAlt: "5", shape: "normal", keycode: 6 },
|
||||
{ keytype: "normal", label: "٦", labelShift: "^", labelAlt: "6", shape: "normal", keycode: 7 },
|
||||
{ keytype: "normal", label: "٧", labelShift: "&", labelAlt: "7", shape: "normal", keycode: 8 },
|
||||
{ keytype: "normal", label: "٨", labelShift: "*", labelAlt: "8", shape: "normal", keycode: 9 },
|
||||
{ keytype: "normal", label: "٩", labelShift: "(", labelAlt: "9", shape: "normal", keycode: 10 },
|
||||
{ keytype: "normal", label: "٠", labelShift: ")", labelAlt: "0", 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 }, // Dad / Fatha
|
||||
{ keytype: "normal", label: "ص", labelShift: "ً", shape: "normal", keycode: 17 }, // Sad / Fathatan
|
||||
{ keytype: "normal", label: "ث", labelShift: "ُ", shape: "normal", keycode: 18 }, // Tha / Damma
|
||||
{ keytype: "normal", label: "ق", labelShift: "ٌ", shape: "normal", keycode: 19 }, // Qaf / Dammatan
|
||||
{ keytype: "normal", label: "ف", labelShift: "لإ", shape: "normal", keycode: 20 }, // Fa / Lam-Alef-Hamza-Below
|
||||
{ keytype: "normal", label: "غ", labelShift: "إ", shape: "normal", keycode: 21 }, // Ghayn / Alef-Hamza-Below
|
||||
{ keytype: "normal", label: "ع", labelShift: "`", shape: "normal", keycode: 22 }, // Ain / Grave
|
||||
{ keytype: "normal", label: "ه", labelShift: "÷", shape: "normal", keycode: 23 }, // Ha / Division
|
||||
{ keytype: "normal", label: "خ", labelShift: "×", shape: "normal", keycode: 24 }, // Kha / Multiplication
|
||||
{ keytype: "normal", label: "ح", labelShift: "؛", shape: "normal", keycode: 25 }, // Hah / Arabic semicolon
|
||||
{ keytype: "normal", label: "ج", labelShift: "<", shape: "normal", keycode: 26 }, // Jeem / <
|
||||
{ keytype: "normal", label: "د", labelShift: ">", shape: "normal", keycode: 27 }, // Dal / >
|
||||
{ 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 }, // Sheen / Kasra
|
||||
{ keytype: "normal", label: "س", labelShift: "ٍ", shape: "normal", keycode: 31 }, // Seen / Kasratan
|
||||
{ keytype: "normal", label: "ي", labelShift: "]", shape: "normal", keycode: 32 }, // Ya / ]
|
||||
{ keytype: "normal", label: "ب", labelShift: "[", shape: "normal", keycode: 33 }, // Ba / [
|
||||
{ keytype: "normal", label: "ل", labelShift: "لأ", shape: "normal", keycode: 34 }, // Lam / Lam-Alef-Hamza-Above
|
||||
{ keytype: "normal", label: "ا", labelShift: "أ", shape: "normal", keycode: 35 }, // Alef / Alef-Hamza-Above
|
||||
{ keytype: "normal", label: "ت", labelShift: "ـ", shape: "normal", keycode: 36 }, // Ta / Tatweel
|
||||
{ keytype: "normal", label: "ن", labelShift: "،", shape: "normal", keycode: 37 }, // Nun / Arabic comma
|
||||
{ keytype: "normal", label: "م", labelShift: "/", shape: "normal", keycode: 38 }, // Meem / Slash
|
||||
{ keytype: "normal", label: "ك", labelShift: ":", shape: "normal", keycode: 39 }, // Kaf / Colon
|
||||
{ keytype: "normal", label: "ط", labelShift: '"', shape: "normal", keycode: 40 }, // Tah / Quote
|
||||
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "shift", keycode: 42 },
|
||||
{ keytype: "normal", label: "ئ", labelShift: "~", shape: "normal", keycode: 44 }, // Hamza-on-Ya / Tilde
|
||||
{ keytype: "normal", label: "ء", labelShift: "ْ", shape: "normal", keycode: 45 }, // Hamza / Sukun
|
||||
{ keytype: "normal", label: "ؤ", labelShift: "{", shape: "normal", keycode: 46 }, // Hamza-on-Waw / {
|
||||
{ keytype: "normal", label: "ر", labelShift: "}", shape: "normal", keycode: 47 }, // Ra / }
|
||||
{ keytype: "normal", label: "لا", labelShift: "لآ", shape: "normal", keycode: 48 }, // Lam-Alef / Lam-Alef-Madda
|
||||
{ keytype: "normal", label: "ى", labelShift: "آ", shape: "normal", keycode: 49 }, // Alef Maksura / Alef-Madda
|
||||
{ keytype: "normal", label: "ة", labelShift: "'", shape: "normal", keycode: 50 }, // Ta Marbuta / Apostrophe
|
||||
{ keytype: "normal", label: "و", labelShift: ",", shape: "normal", keycode: 51 }, // Waw / Comma
|
||||
{ keytype: "normal", label: "ز", labelShift: ".", shape: "normal", keycode: 52 }, // Zay / Period
|
||||
{ keytype: "normal", label: "ظ", labelShift: "؟", shape: "normal", keycode: 53 }, // Zha / Arabic question mark
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", 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 }
|
||||
]
|
||||
]
|
||||
},
|
||||
"Persian": {
|
||||
name_short: "FA",
|
||||
description: "Persian ISIRI 9147 - Full",
|
||||
comment: "Standard Persian keyboard layout (XKB ir(pes), ISIRI 9147)",
|
||||
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: "ZWJ", labelShift: "÷", labelAlt: "~", shape: "normal", keycode: 41 }, // Zero-Width Joiner / division
|
||||
{ keytype: "normal", label: "۱", labelShift: "!", labelAlt: "1", shape: "normal", keycode: 2 },
|
||||
{ keytype: "normal", label: "۲", labelShift: "٬", labelAlt: "2", shape: "normal", keycode: 3 }, // Arabic thousands separator
|
||||
{ keytype: "normal", label: "۳", labelShift: "٫", labelAlt: "3", shape: "normal", keycode: 4 }, // Arabic decimal separator
|
||||
{ keytype: "normal", label: "۴", labelShift: "﷼", labelAlt: "4", shape: "normal", keycode: 5 }, // Rial sign
|
||||
{ keytype: "normal", label: "۵", labelShift: "٪", labelAlt: "5", shape: "normal", keycode: 6 }, // Arabic percent
|
||||
{ keytype: "normal", label: "۶", labelShift: "×", labelAlt: "6", shape: "normal", keycode: 7 },
|
||||
{ keytype: "normal", label: "۷", labelShift: "،", labelAlt: "7", shape: "normal", keycode: 8 }, // Arabic comma
|
||||
{ keytype: "normal", label: "۸", labelShift: "*", labelAlt: "8", shape: "normal", keycode: 9 },
|
||||
{ keytype: "normal", label: "۹", labelShift: ")", labelAlt: "9", shape: "normal", keycode: 10 }, // RTL-swapped parens
|
||||
{ keytype: "normal", label: "۰", labelShift: "(", labelAlt: "0", shape: "normal", keycode: 11 }, // RTL-swapped parens
|
||||
{ keytype: "normal", label: "-", labelShift: "ـ", shape: "normal", keycode: 12 }, // minus / tatweel
|
||||
{ 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 }, // Dad / Sukun
|
||||
{ keytype: "normal", label: "ص", labelShift: "ٌ", shape: "normal", keycode: 17 }, // Sad / Dammatan
|
||||
{ keytype: "normal", label: "ث", labelShift: "ٍ", labelAlt: "€", shape: "normal", keycode: 18 }, // Theh / Kasratan
|
||||
{ keytype: "normal", label: "ق", labelShift: "ً", shape: "normal", keycode: 19 }, // Qaf / Fathatan
|
||||
{ keytype: "normal", label: "ف", labelShift: "ُ", shape: "normal", keycode: 20 }, // Feh / Damma
|
||||
{ keytype: "normal", label: "غ", labelShift: "ِ", shape: "normal", keycode: 21 }, // Ghain / Kasra
|
||||
{ keytype: "normal", label: "ع", labelShift: "َ", shape: "normal", keycode: 22 }, // Ain / Fatha
|
||||
{ keytype: "normal", label: "ه", labelShift: "ّ", shape: "normal", keycode: 23 }, // Heh / Shadda
|
||||
{ keytype: "normal", label: "خ", labelShift: "]", shape: "normal", keycode: 24 }, // Khah / ]
|
||||
{ keytype: "normal", label: "ح", labelShift: "[", shape: "normal", keycode: 25 }, // Hah / [
|
||||
{ keytype: "normal", label: "ج", labelShift: "}", shape: "normal", keycode: 26 }, // Jeem / }
|
||||
{ keytype: "normal", label: "چ", labelShift: "{", shape: "normal", keycode: 27 }, // Tcheh / {
|
||||
{ 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 }, // Sheen / Hamza-on-Waw
|
||||
{ keytype: "normal", label: "س", labelShift: "ئ", shape: "normal", keycode: 31 }, // Seen / Hamza-on-Yeh
|
||||
{ keytype: "normal", label: "ی", labelShift: "ي", shape: "normal", keycode: 32 }, // Farsi Yeh / Arabic Yeh
|
||||
{ keytype: "normal", label: "ب", labelShift: "إ", shape: "normal", keycode: 33 }, // Beh / Alef-Hamza-Below
|
||||
{ keytype: "normal", label: "ل", labelShift: "أ", shape: "normal", keycode: 34 }, // Lam / Alef-Hamza-Above
|
||||
{ keytype: "normal", label: "ا", labelShift: "آ", shape: "normal", keycode: 35 }, // Alef / Alef-Madda
|
||||
{ keytype: "normal", label: "ت", labelShift: "ة", shape: "normal", keycode: 36 }, // Teh / Teh Marbuta
|
||||
{ keytype: "normal", label: "ن", labelShift: "»", shape: "normal", keycode: 37 }, // Noon / Right guillemet
|
||||
{ keytype: "normal", label: "م", labelShift: "«", shape: "normal", keycode: 38 }, // Meem / Left guillemet
|
||||
{ keytype: "normal", label: "ک", labelShift: ":", shape: "normal", keycode: 39 }, // Keheh / Colon
|
||||
{ keytype: "normal", label: "گ", labelShift: "؛", shape: "normal", keycode: 40 }, // Gaf / Arabic semicolon
|
||||
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
|
||||
],
|
||||
[
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "shift", keycode: 42 },
|
||||
{ keytype: "normal", label: "ظ", labelShift: "ك", shape: "normal", keycode: 44 }, // Zah / Arabic Kaf
|
||||
{ keytype: "normal", label: "ط", labelShift: "ٓ", shape: "normal", keycode: 45 }, // Tah / Maddah above
|
||||
{ keytype: "normal", label: "ز", labelShift: "ژ", shape: "normal", keycode: 46 }, // Zain / Jeh (Persian Zhe)
|
||||
{ keytype: "normal", label: "ر", labelShift: "ٰ", shape: "normal", keycode: 47 }, // Ra / Superscript Alef
|
||||
{ keytype: "normal", label: "ذ", labelShift: "ZWNJ", shape: "normal", keycode: 48 }, // Thal / Zero-Width Non-Joiner
|
||||
{ keytype: "normal", label: "د", labelShift: "ٔ", shape: "normal", keycode: 49 }, // Dal / Hamza above
|
||||
{ keytype: "normal", label: "پ", labelShift: "ء", shape: "normal", keycode: 50 }, // Peh / Hamza
|
||||
{ keytype: "normal", label: "و", labelShift: ">", shape: "normal", keycode: 51 }, // Waw / >
|
||||
{ keytype: "normal", label: ".", labelShift: "<", shape: "normal", keycode: 52 }, // Period / <
|
||||
{ keytype: "normal", label: "/", labelShift: "؟", shape: "normal", keycode: 53 }, // Slash / Arabic question mark
|
||||
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", 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 }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
94
surfaces/quickshell/ii-base/modules/ii/overlay/Overlay.qml
Normal file
94
surfaces/quickshell/ii-base/modules/ii/overlay/Overlay.qml
Normal file
|
|
@ -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.m3colors.m3surfaceContainer
|
||||
}
|
||||
|
|
@ -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,44 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
signal requestCenter(string identifier)
|
||||
|
||||
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: "notes", 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,151 @@
|
|||
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)
|
||||
altAction: () => OverlayContext.requestCenter(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.notes
|
||||
|
||||
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: "notes"; Notes {} }
|
||||
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
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();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: OverlayContext
|
||||
function onRequestCenter(identifier) {
|
||||
if (identifier === root.identifier) {
|
||||
root.center()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.colLayer1Base, (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.colLayer1Base
|
||||
// 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,98 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
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: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
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,17 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
StyledOverlayWidget {
|
||||
id: root
|
||||
title: Translation.tr("Notes")
|
||||
showCenterButton: true
|
||||
|
||||
contentItem: NotesContent {
|
||||
radius: root.contentRadius
|
||||
isClickthrough: root.clickthrough
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.ii.overlay
|
||||
|
||||
OverlayBackground {
|
||||
id: root
|
||||
|
||||
property alias content: textInput.text
|
||||
property bool pendingReload: false
|
||||
property var copyListEntries: []
|
||||
property string lastParsedCopylistText: ""
|
||||
property var parsedCopylistLines: []
|
||||
property bool isClickthrough: false
|
||||
property real maxCopyButtonSize: 20
|
||||
|
||||
Component.onCompleted: {
|
||||
noteFile.reload();
|
||||
updateCopyListEntries();
|
||||
}
|
||||
|
||||
function saveContent() {
|
||||
if (!textInput)
|
||||
return;
|
||||
noteFile.setText(root.content);
|
||||
}
|
||||
|
||||
function focusAtEnd() {
|
||||
if (!textInput)
|
||||
return;
|
||||
textInput.forceActiveFocus();
|
||||
const endPos = root.content.length;
|
||||
applySelection(endPos, endPos);
|
||||
}
|
||||
|
||||
function applySelection(cursorPos, anchorPos) {
|
||||
if (!textInput)
|
||||
return;
|
||||
const textLength = root.content.length;
|
||||
const cursor = Math.max(0, Math.min(cursorPos, textLength));
|
||||
const anchor = Math.max(0, Math.min(anchorPos, textLength));
|
||||
textInput.select(anchor, cursor);
|
||||
if (cursor === anchor)
|
||||
textInput.deselect();
|
||||
}
|
||||
|
||||
function scheduleCopylistUpdate(immediate = false) {
|
||||
if (!textInput)
|
||||
return;
|
||||
if (immediate) {
|
||||
copyListDebounce?.stop();
|
||||
updateCopyListEntries();
|
||||
} else {
|
||||
copyListDebounce.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCopyListEntries() {
|
||||
if (!textInput)
|
||||
return;
|
||||
const textValue = root.content;
|
||||
if (!textValue || textValue.length === 0) {
|
||||
lastParsedCopylistText = "";
|
||||
parsedCopylistLines = [];
|
||||
root.copyListEntries = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (textValue !== lastParsedCopylistText) {
|
||||
const lineRegex = /(.*?)(\r?\n|$)/g;
|
||||
let match = null;
|
||||
const parsed = [];
|
||||
while ((match = lineRegex.exec(textValue)) !== null) {
|
||||
const lineText = match[1];
|
||||
const newlineText = match[2];
|
||||
const lineStart = match.index;
|
||||
const lineEnd = lineStart + lineText.length;
|
||||
const bulletMatch = lineText.match(/^\s*-\s+(.*\S)\s*$/);
|
||||
if (bulletMatch) {
|
||||
parsed.push({
|
||||
content: bulletMatch[1].trim(),
|
||||
start: lineStart,
|
||||
end: lineEnd
|
||||
});
|
||||
}
|
||||
if (newlineText === "")
|
||||
break;
|
||||
}
|
||||
lastParsedCopylistText = textValue;
|
||||
parsedCopylistLines = parsed;
|
||||
if (parsed.length === 0) {
|
||||
root.copyListEntries = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
updateCopylistPositions();
|
||||
}
|
||||
|
||||
function updateCopylistPositions() {
|
||||
if (!textInput || parsedCopylistLines.length === 0)
|
||||
return;
|
||||
const rawSelectionStart = textInput.selectionStart;
|
||||
const rawSelectionEnd = textInput.selectionEnd;
|
||||
const selectionStart = rawSelectionStart === -1 ? textInput.cursorPosition : rawSelectionStart;
|
||||
const selectionEnd = rawSelectionEnd === -1 ? textInput.cursorPosition : rawSelectionEnd;
|
||||
const rangeStart = Math.min(selectionStart, selectionEnd);
|
||||
const rangeEnd = Math.max(selectionStart, selectionEnd);
|
||||
|
||||
const entries = parsedCopylistLines.map(line => {
|
||||
// Don't show copy button if line is (partially) selected
|
||||
const caretIntersects = rangeEnd > line.start && rangeStart <= line.end;
|
||||
if (caretIntersects)
|
||||
return null;
|
||||
const startRect = textInput.positionToRectangle(line.start);
|
||||
let endRect = textInput.positionToRectangle(line.end);
|
||||
if (!isFinite(startRect.y))
|
||||
return null;
|
||||
if (!isFinite(endRect.y))
|
||||
endRect = startRect;
|
||||
const lineBottom = endRect.y + endRect.height;
|
||||
const rectHeight = Math.max(lineBottom - startRect.y, textInput.font.pixelSize + 8);
|
||||
return {
|
||||
content: line.content,
|
||||
y: startRect.y,
|
||||
height: rectHeight
|
||||
};
|
||||
}).filter(entry => entry !== null);
|
||||
|
||||
root.copyListEntries = entries;
|
||||
}
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: 200
|
||||
|
||||
ColumnLayout {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
spacing: -16
|
||||
|
||||
ScrollView {
|
||||
id: editorScrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
onWidthChanged: root.scheduleCopylistUpdate(true)
|
||||
|
||||
StyledTextArea { // This has to be a direct child of ScrollView for proper scrolling
|
||||
id: textInput
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
wrapMode: TextEdit.Wrap
|
||||
placeholderText: Translation.tr("Write something here...\nUse '-' to create copyable bullet points, like this:\n\nSheep fricker\n- 4x Slab\n- 1x Boat\n- 4x Redstone Dust\n- 1x Sticky Piston\n- 1x End Rod\n- 4x Redstone Repeater\n- 1x Redstone Torch\n- 1x Sheep")
|
||||
selectByMouse: true
|
||||
persistentSelection: true
|
||||
textFormat: TextEdit.PlainText
|
||||
background: null
|
||||
padding: 24
|
||||
|
||||
onTextChanged: {
|
||||
if (textInput.activeFocus) {
|
||||
saveDebounce.restart();
|
||||
}
|
||||
root.scheduleCopylistUpdate(true);
|
||||
}
|
||||
|
||||
onHeightChanged: root.scheduleCopylistUpdate(true)
|
||||
onContentHeightChanged: root.scheduleCopylistUpdate(true)
|
||||
onCursorPositionChanged: root.scheduleCopylistUpdate()
|
||||
onSelectionStartChanged: root.scheduleCopylistUpdate()
|
||||
onSelectionEndChanged: root.scheduleCopylistUpdate()
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.copyListEntries.length > 0
|
||||
clip: true
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.copyListEntries
|
||||
}
|
||||
delegate: RippleButton {
|
||||
id: copyButton
|
||||
required property var modelData
|
||||
readonly property real lineHeight: Math.min(Math.max(modelData.height, Appearance.font.pixelSize.normal + 6), root.maxCopyButtonSize)
|
||||
readonly property real iconSizeLocal: Appearance.font.pixelSize.normal
|
||||
readonly property real hitPadding: 6
|
||||
property bool justCopied: false
|
||||
|
||||
implicitHeight: lineHeight
|
||||
implicitWidth: lineHeight
|
||||
buttonRadius: height / 2
|
||||
y: modelData.y
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
z: 5
|
||||
|
||||
Timer {
|
||||
id: resetState
|
||||
interval: 700
|
||||
onTriggered: {
|
||||
copyButton.justCopied = false;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = copyButton.modelData.content;
|
||||
justCopied = true;
|
||||
resetState.start();
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
MaterialSymbol {
|
||||
id: iconItem
|
||||
anchors.centerIn: parent
|
||||
text: copyButton.justCopied ? "check" : "content_copy"
|
||||
iconSize: copyButton.iconSizeLocal
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: statusLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: saveDebounce.running ? Translation.tr("Saving...") : Translation.tr("Saved ")
|
||||
color: Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveDebounce
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: saveContent()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyListDebounce
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: updateCopylistPositions()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: noteFile
|
||||
path: Qt.resolvedUrl(Directories.notesPath)
|
||||
onLoaded: {
|
||||
root.content = noteFile.text();
|
||||
if (root.content !== root.content) {
|
||||
const previousCursor = textInput.cursorPosition;
|
||||
const previousAnchor = textInput.selectionStart;
|
||||
root.content = root.content;
|
||||
applySelection(previousCursor, previousAnchor);
|
||||
}
|
||||
if (pendingReload) {
|
||||
pendingReload = false;
|
||||
Qt.callLater(root.focusAtEnd);
|
||||
}
|
||||
Qt.callLater(root.updateCopyListEntries);
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (error === FileViewError.FileNotFound) {
|
||||
root.content = "";
|
||||
noteFile.setText(root.content);
|
||||
if (pendingReload) {
|
||||
pendingReload = false;
|
||||
Qt.callLater(root.focusAtEnd);
|
||||
}
|
||||
Qt.callLater(root.updateCopyListEntries);
|
||||
} else {
|
||||
console.log("[Overlay Notes] Error loading file: " + error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
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: Translation.tr("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,131 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Qt5Compat.GraphicalEffects
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
214
surfaces/quickshell/ii-base/modules/ii/overview/Overview.qml
Normal file
214
surfaces/quickshell/ii-base/modules/ii/overview/Overview.qml
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
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
|
||||
|
||||
PanelWindow {
|
||||
id: panelWindow
|
||||
property string searchingText: ""
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
|
||||
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
|
||||
visible: GlobalStates.overviewOpen
|
||||
|
||||
WlrLayershell.namespace: "quickshell:overview"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
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
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onOverviewOpenChanged() {
|
||||
if (!GlobalStates.overviewOpen) {
|
||||
searchWidget.disableExpandAnimation();
|
||||
overviewScope.dontAutoCancelSearch = false;
|
||||
GlobalFocusGrab.dismiss();
|
||||
} else {
|
||||
if (!overviewScope.dontAutoCancelSearch) {
|
||||
searchWidget.cancelSearch();
|
||||
}
|
||||
GlobalFocusGrab.addDismissable(panelWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GlobalFocusGrab
|
||||
function onDismissed() {
|
||||
GlobalStates.overviewOpen = false;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
SearchWidget {
|
||||
id: searchWidget
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Synchronizer on searchingText {
|
||||
property alias source: panelWindow.searchingText
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overviewLoader
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
|
||||
sourceComponent: OverviewWidget {
|
||||
screen: panelWindow.screen
|
||||
visible: (panelWindow.searchingText == "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleClipboard() {
|
||||
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
|
||||
GlobalStates.overviewOpen = false;
|
||||
return;
|
||||
}
|
||||
overviewScope.dontAutoCancelSearch = true;
|
||||
panelWindow.setSearchingText(Config.options.search.prefix.clipboard);
|
||||
GlobalStates.overviewOpen = true;
|
||||
}
|
||||
|
||||
function toggleEmojis() {
|
||||
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
|
||||
GlobalStates.overviewOpen = false;
|
||||
return;
|
||||
}
|
||||
overviewScope.dontAutoCancelSearch = true;
|
||||
panelWindow.setSearchingText(Config.options.search.prefix.emojis);
|
||||
GlobalStates.overviewOpen = true;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "search"
|
||||
|
||||
function toggle() {
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
function workspacesToggle() {
|
||||
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: "searchToggle"
|
||||
description: "Toggles search on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "overviewWorkspacesClose"
|
||||
description: "Closes overview on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.overviewOpen = false;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "overviewWorkspacesToggle"
|
||||
description: "Toggles overview on press"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "searchToggleRelease"
|
||||
description: "Toggles search on release"
|
||||
|
||||
onPressed: {
|
||||
GlobalStates.superReleaseMightTrigger = true;
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (GlobalStates.shouldSuppressSuperReleaseSearch() || !GlobalStates.superReleaseMightTrigger) {
|
||||
GlobalStates.superReleaseMightTrigger = true;
|
||||
return;
|
||||
}
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "searchToggleReleaseInterrupt"
|
||||
description: "Interrupts possibility of search 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue