Merge branch 'end-4:main' into parallax

This commit is contained in:
Ivan Rosinskii
2025-12-23 23:35:22 +01:00
committed by GitHub
53 changed files with 1215 additions and 200 deletions
@@ -30,13 +30,9 @@ Singleton {
let y = 0.5768 * (x * x) - 0.759 * (x) + 0.2896
return Math.max(0, Math.min(0.22, y))
}
property real autoContentTransparency: { // y = -10.1734x^2 + 3.4457x + 0.1872
let x = autoBackgroundTransparency
let y = -10.1734 * (x * x) + 3.4457 * (x) + 0.1872
return Math.max(0, Math.min(0.6, y))
}
property real autoContentTransparency: 0.9
property real backgroundTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoBackgroundTransparency : Config?.options.appearance.transparency.backgroundTransparency : 0
property real contentTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency : 0
property real contentTransparency: Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency
m3colors: QtObject {
property bool darkmode: true
@@ -114,30 +110,41 @@ Singleton {
colors: QtObject {
property color colSubtext: m3colors.m3outline
property color colLayer0: ColorUtils.mix(ColorUtils.transparentize(m3colors.m3background, root.backgroundTransparency), m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1)
// Layer 0
property color colLayer0Base: ColorUtils.mix(m3colors.m3background, m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1)
property color colLayer0: ColorUtils.transparentize(colLayer0Base, root.backgroundTransparency)
property color colOnLayer0: m3colors.m3onBackground
property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency))
property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency))
property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
property color colLayer1: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency);
// Layer 1
property color colLayer1Base: m3colors.m3surfaceContainerLow
property color colLayer1: ColorUtils.solveOverlayColor(colLayer0Base, colLayer1Base, 1 - root.contentTransparency);
property color colOnLayer1: m3colors.m3onSurfaceVariant;
property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45);
property color colLayer2: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.contentTransparency)
property color colOnLayer2: m3colors.m3onSurface;
property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4);
property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency)
property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency);
property color colLayer2Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.90), root.contentTransparency)
property color colLayer2Active: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.80), root.contentTransparency);
property color colLayer2Disabled: ColorUtils.transparentize(ColorUtils.mix(colLayer2, m3colors.m3background, 0.8), root.contentTransparency);
property color colLayer3: ColorUtils.transparentize(m3colors.m3surfaceContainerHigh, root.contentTransparency)
// Layer 2
property color colLayer2Base: m3colors.m3surfaceContainer
property color colLayer2: ColorUtils.solveOverlayColor(colLayer1Base, colLayer2Base, 1 - root.contentTransparency)
property color colLayer2Hover: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.90), 1 - root.contentTransparency)
property color colLayer2Active: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.80), 1 - root.contentTransparency);
property color colLayer2Disabled: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, m3colors.m3background, 0.8), 1 - root.contentTransparency);
property color colOnLayer2: m3colors.m3onSurface;
property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4);
// Layer 3
property color colLayer3Base: m3colors.m3surfaceContainerHigh
property color colLayer3: ColorUtils.solveOverlayColor(colLayer2Base, colLayer3Base, 1 - root.contentTransparency)
property color colLayer3Hover: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.90), 1 - root.contentTransparency)
property color colLayer3Active: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.80), 1 - root.contentTransparency);
property color colOnLayer3: m3colors.m3onSurface;
property color colLayer3Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.90), root.contentTransparency)
property color colLayer3Active: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.80), root.contentTransparency);
property color colLayer4: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency)
// Layer 4
property color colLayer4Base: m3colors.m3surfaceContainerHighest
property color colLayer4: ColorUtils.solveOverlayColor(colLayer3Base, colLayer4Base, 1 - root.contentTransparency)
property color colLayer4Hover: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.90), 1 - root.contentTransparency)
property color colLayer4Active: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.80), 1 - root.contentTransparency);
property color colOnLayer4: m3colors.m3onSurface;
property color colLayer4Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer4, colOnLayer4, 0.90), root.contentTransparency)
property color colLayer4Active: ColorUtils.transparentize(ColorUtils.mix(colLayer4, colOnLayer4, 0.80), root.contentTransparency);
// Primary
property color colPrimary: m3colors.m3primary
property color colOnPrimary: m3colors.m3onPrimary
property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87)
@@ -146,13 +153,16 @@ Singleton {
property color colPrimaryContainerHover: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.9)
property color colPrimaryContainerActive: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.8)
property color colOnPrimaryContainer: m3colors.m3onPrimaryContainer
// Secondary
property color colSecondary: m3colors.m3secondary
property color colOnSecondary: m3colors.m3onSecondary
property color colSecondaryHover: ColorUtils.mix(m3colors.m3secondary, colLayer1Hover, 0.85)
property color colSecondaryActive: ColorUtils.mix(m3colors.m3secondary, colLayer1Active, 0.4)
property color colOnSecondary: m3colors.m3onSecondary
property color colSecondaryContainer: m3colors.m3secondaryContainer
property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.90)
property color colSecondaryContainerActive: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.54)
property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
// Tertiary
property color colTertiary: m3colors.m3tertiary
property color colTertiaryHover: ColorUtils.mix(m3colors.m3tertiary, colLayer1Hover, 0.85)
property color colTertiaryActive: ColorUtils.mix(m3colors.m3tertiary, colLayer1Active, 0.4)
@@ -161,16 +171,17 @@ Singleton {
property color colTertiaryContainerActive: ColorUtils.mix(m3colors.m3tertiaryContainer, colLayer1Active, 0.54)
property color colOnTertiary: m3colors.m3onTertiary
property color colOnTertiaryContainer: m3colors.m3onTertiaryContainer
property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
property color colSurfaceContainerLow: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency)
property color colSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.contentTransparency)
// Surface
property color colBackgroundSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.backgroundTransparency)
property color colSurfaceContainerHigh: ColorUtils.transparentize(m3colors.m3surfaceContainerHigh, root.contentTransparency)
property color colSurfaceContainerHighest: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency)
property color colSurfaceContainerLow: ColorUtils.solveOverlayColor(m3colors.m3background, m3colors.m3surfaceContainerLow, 1 - root.contentTransparency)
property color colSurfaceContainer: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerLow, m3colors.m3surfaceContainer, 1 - root.contentTransparency)
property color colSurfaceContainerHigh: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainer, m3colors.m3surfaceContainerHigh, 1 - root.contentTransparency)
property color colSurfaceContainerHighest: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerHigh, m3colors.m3surfaceContainerHighest, 1 - root.contentTransparency)
property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95)
property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85)
property color colOnSurface: m3colors.m3onSurface
property color colOnSurfaceVariant: m3colors.m3onSurfaceVariant
// Misc
property color colTooltip: m3colors.m3inverseSurface
property color colOnTooltip: m3colors.m3inverseOnSurface
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
@@ -135,4 +135,38 @@ Singleton {
var c = Qt.color(color);
return c.hslLightness < 0.5;
}
/**
* Clamps a value to the inclusive range [0, 1].
*
* @param {number} x - The value to clamp.
* @returns {number} The clamped value in the range [0, 1].
*/
function clamp01(x) {
return Math.min(1, Math.max(0, x));
}
/**
* Solves for the solid overlay color that, when composited over a base color
* with a given opacity, yields the target color.
*
* The compositing equation is:
* result = overlay * overlayOpacity + base * (1 - overlayOpacity)
*
* This function algebraically inverts that equation per channel.
*
* @param {Qt.rgba} baseColor - The base (background) color.
* @param {Qt.rgba} targetColor - The resulting color after compositing.
* @param {number} overlayOpacity - The overlay opacity (0-1).
* @returns {Qt.rgba} The solved overlay color
*/
function solveOverlayColor(baseColor, targetColor, overlayOpacity) {
let invA = 1.0 - overlayOpacity;
let r = (targetColor.r - baseColor.r * invA) / overlayOpacity;
let g = (targetColor.g - baseColor.g * invA) / overlayOpacity;
let b = (targetColor.b - baseColor.b * invA) / overlayOpacity;
return Qt.rgba(clamp01(r), clamp01(g), clamp01(b), overlayOpacity);
}
}
@@ -0,0 +1,81 @@
pragma ComponentBehavior: Bound
pragma Singleton
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import Qt.labs.synchronizer
import Quickshell
Singleton {
id: root
enum Action {
Copy,
Edit,
Search,
CharRecognition,
Record,
RecordWithSound
}
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload"
function getCommand(x, y, width, height, screenshotPath, action, saveDir = "") {
// Set command for action
const rx = Math.round(x);
const ry = Math.round(y);
const rw = Math.round(width);
const rh = Math.round(height);
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} `
+ `-crop ${rw}x${rh}+${rx}+${ry}`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`;
switch (action) {
case ScreenshotAction.Action.Copy:
if (saveDir === "") {
// not saving the screenshot, just copy to clipboard
return ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
}
return [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(saveDir)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${saveDir}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
]
break;
case ScreenshotAction.Action.Edit:
return ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`]
break;
case ScreenshotAction.Action.Search:
return ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(screenshotPath)})" && ${cleanup}`]
break;
case ScreenshotAction.Action.CharRecognition:
return ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
break;
case ScreenshotAction.Action.Record:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
break;
case ScreenshotAction.Action.RecordWithSound:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
return;
}
}
}
@@ -0,0 +1,14 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.functions
Process {
id: screenshotProc
running: true
property string screenshotDir: Directories.screenshotTemp
required property ShellScreen screen
property string screenshotPath: `${screenshotDir}/image-${screen.name}`
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(screen.name)}' '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`]
}
@@ -0,0 +1,28 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
Canvas {
id: root
property color color: "#ffffff"
property int dashLength: 6
property int gapLength: 4
property int borderWidth: 1
onDashLengthChanged: requestPaint()
onGapLengthChanged: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.strokeStyle = root.color;
ctx.lineWidth = root.borderWidth;
if (root.gapLength > 0) {
ctx.setLineDash([root.dashLength, root.gapLength]); // Set dash pattern
}
ctx.strokeRect(root.borderWidth / 2, root.borderWidth / 2, width - root.borderWidth, height - root.borderWidth); // Draw it
ctx.restore();
}
}
@@ -14,12 +14,16 @@ MouseArea {
property bool automaticallyReset: true
readonly property real dragDiffX: _dragDiffX
readonly property real dragDiffY: _dragDiffY
property real startX: 0
property real startY: 0
property real regionTopLeftX: Math.min(startX, startX + _dragDiffX)
property real regionTopLeftY: Math.min(startY, startY + _dragDiffY)
property real regionWidth: Math.abs(_dragDiffX)
property real regionHeight: Math.abs(_dragDiffY)
signal dragPressed(diffX: real, diffY: real)
signal dragReleased(diffX: real, diffY: real)
property real startX: 0
property real startY: 0
property bool dragging: false
property real _dragDiffX: 0
property real _dragDiffY: 0
@@ -121,7 +121,7 @@ MouseArea { // Notification group area
id: background
anchors.left: parent.left
width: parent.width
color: popup ? ColorUtils.applyAlpha(Appearance.colors.colLayer2, 1 - Appearance.backgroundTransparency) : Appearance.colors.colLayer2
color: popup ? Appearance.colors.colBackgroundSurfaceContainer : Appearance.colors.colLayer2
radius: Appearance.rounding.normal
anchors.leftMargin: root.xOffset
@@ -12,19 +12,30 @@ Item {
required property var tabButtonList
function incrementCurrentIndex() {
tabBar.incrementCurrentIndex()
tabBar.incrementCurrentIndex();
}
function decrementCurrentIndex() {
tabBar.decrementCurrentIndex()
tabBar.decrementCurrentIndex();
}
function setCurrentIndex(index) {
tabBar.setCurrentIndex(index)
tabBar.setCurrentIndex(index);
}
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
implicitWidth: contentItem.implicitWidth
implicitHeight: 40
property Component delegate: ToolbarTabButton {
required property int index
required property var modelData
current: index == root.currentIndex
text: modelData.name
materialSymbol: modelData.icon
onClicked: {
root.setCurrentIndex(index);
}
}
Row {
id: contentItem
z: 1
@@ -33,16 +44,7 @@ Item {
Repeater {
model: root.tabButtonList
delegate: ToolbarTabButton {
required property int index
required property var modelData
current: index == root.currentIndex
text: modelData.name
materialSymbol: modelData.icon
onClicked: {
root.setCurrentIndex(index)
}
}
delegate: root.delegate
}
}
@@ -76,23 +78,23 @@ Item {
z: 2
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
onWheel: (event) => {
onWheel: event => {
if (event.angleDelta.y < 0) {
root.incrementCurrentIndex();
}
else {
} else {
root.decrementCurrentIndex();
}
}
}
// TabBar doesn't allow tabs to be of different sizes. Literally unusable.
// TabBar doesn't allow tabs to be of different sizes. That's what I thought...
// We use it only for the logic and draw stuff manually
TabBar {
id: tabBar
z: -1
background: null
Repeater { // This is to fool the TabBar that it has tabs so it does the indices properly
Repeater {
// This is to fool the TabBar that it has tabs so it does the indices properly
model: root.tabButtonList.length
delegate: TabButton {
background: null
@@ -4,5 +4,5 @@ import qs.modules.common
Rectangle {
id: contentItem
anchors.fill: parent
color: Appearance.colors.colSurfaceContainer
color: Appearance.m3colors.m3surfaceContainer
}
@@ -190,7 +190,7 @@ AbstractOverlayWidget {
fill: parent
margins: root.resizeMargin
}
color: ColorUtils.transparentize(Appearance.colors.colLayer1, (root.fancyBorders && GlobalStates.overlayOpen) ? 0 : 1)
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
@@ -217,7 +217,7 @@ AbstractOverlayWidget {
Layout.fillWidth: true
implicitWidth: titleBarRow.implicitWidth + root.padding * 2
implicitHeight: titleBarRow.implicitHeight + root.padding * 2
color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1
color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1Base
// border.color: Appearance.colors.colOutlineVariant
// border.width: 1
@@ -192,6 +192,14 @@ Scope {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "overviewWorkspacesClose"
description: "Closes overview on press"
onPressed: {
GlobalStates.overviewOpen = false;
}
}
GlobalShortcut {
name: "overviewWorkspacesToggle"
description: "Toggles overview on press"
@@ -101,7 +101,7 @@ Item {
required property int index
property int colIndex: index
property int workspaceValue: root.workspaceGroup * root.workspacesShown + getWsInCell(row.index, colIndex)
property color defaultWorkspaceColor: ColorUtils.mix(Appearance.colors.colBackgroundSurfaceContainer, Appearance.colors.colSurfaceContainerHigh, 0.8)
property color defaultWorkspaceColor: Appearance.colors.colSurfaceContainerLow
property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
property color hoveredBorderColor: Appearance.colors.colLayer2Hover
property bool hoveredWhileDragging: false
@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
@@ -32,14 +33,8 @@ PanelWindow {
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
signal dismiss()
property string saveScreenshotDir: Config.options.screenSnip.savePath !== ""
? Config.options.screenSnip.savePath
: ""
property string screenshotDir: Directories.screenshotTemp
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload"
property color overlayColor: "#88111111"
property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0
property color brightSecondary: Appearance.m3colors.darkmode ? Appearance.colors.colSecondary : Appearance.colors.colOnSecondary
@@ -180,10 +175,12 @@ PanelWindow {
property real regionX: Math.min(dragStartX, draggingX)
property real regionY: Math.min(dragStartY, draggingY)
Process {
TempScreenshotProcess {
id: screenshotProc
running: true
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(root.screen.name)}' '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`]
screen: root.screen
screenshotDir: root.screenshotDir
screenshotPath: root.screenshotPath
onExited: (exitCode, exitStatus) => {
if (root.enableContentRegions) imageDetectionProcess.running = true;
root.preparationDone = !checkRecordingProc.running;
@@ -229,6 +226,27 @@ PanelWindow {
}
}
function getScreenshotAction() {
switch(root.action) {
case RegionSelection.SnipAction.Copy:
return ScreenshotAction.Action.Copy;
case RegionSelection.SnipAction.Edit:
return ScreenshotAction.Action.Edit;
case RegionSelection.SnipAction.Search:
return ScreenshotAction.Action.Search;
case RegionSelection.SnipAction.CharRecognition:
return ScreenshotAction.Action.CharRecognition;
case RegionSelection.SnipAction.Record:
return ScreenshotAction.Action.Record;
case RegionSelection.SnipAction.RecordWithSound:
return ScreenshotAction.Action.RecordWithSound;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
root.dismiss();
return;
}
}
function snip() {
// Validity check
if (root.regionWidth <= 0 || root.regionHeight <= 0) {
@@ -246,62 +264,20 @@ PanelWindow {
if (root.action === RegionSelection.SnipAction.Copy || root.action === RegionSelection.SnipAction.Edit) {
root.action = root.mouseButton === Qt.RightButton ? RegionSelection.SnipAction.Edit : RegionSelection.SnipAction.Copy;
}
// Set command for action
const rx = Math.round(root.regionX * root.monitorScale);
const ry = Math.round(root.regionY * root.monitorScale);
const rw = Math.round(root.regionWidth * root.monitorScale);
const rh = Math.round(root.regionHeight * root.monitorScale);
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} `
+ `-crop ${rw}x${rh}+${rx}+${ry}`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`;
switch (root.action) {
case RegionSelection.SnipAction.Copy:
if (saveScreenshotDir === "") {
// not saving the screenshot, just copy to clipboard
snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
}
const savePathBase = root.saveScreenshotDir
snipProc.command = [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(savePathBase)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${savePathBase}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
]
break;
case RegionSelection.SnipAction.Edit:
snipProc.command = ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`]
break;
case RegionSelection.SnipAction.Search:
snipProc.command = ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(root.screenshotPath)})" && ${cleanup}`]
break;
case RegionSelection.SnipAction.CharRecognition:
snipProc.command = ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
break;
case RegionSelection.SnipAction.Record:
snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
break;
case RegionSelection.SnipAction.RecordWithSound:
snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
root.dismiss();
return;
}
const screenshotDir = Config.options.screenSnip.savePath !== "" ? //
Config.options.screenSnip.savePath : "";
var screenshotAction = root.getScreenshotAction();
const command = ScreenshotAction.getCommand(
root.regionX * root.monitorScale, //
root.regionY * root.monitorScale, //
root.regionWidth * root.monitorScale,//
root.regionHeight * root.monitorScale, //
root.screenshotPath, //
screenshotAction, //
screenshotDir
)
snipProc.command = command;
// Image post-processing
snipProc.startDetached();
@@ -25,6 +25,10 @@ Rectangle {
border.width: targeted ? 4 : 2
radius: 4
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
@@ -45,9 +45,7 @@ Scope {
WlrLayershell.namespace: "quickshell:session"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
// This is a big surface so we needa carefully choose the transparency,
// or we'll get a large scary rgb blob
color: ColorUtils.transparentize(Appearance.m3colors.m3background, Appearance.m3colors.darkmode ? 0.04 : 0.12)
color: ColorUtils.transparentize(Appearance.m3colors.m3background, Appearance.m3colors.darkmode ? 0.05 : 0.12)
anchors {
top: true
@@ -314,7 +314,10 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
implicitWidth: statusRowLayout.implicitWidth + 10 * 2
implicitHeight: Math.max(statusRowLayout.implicitHeight, 38)
radius: Appearance.rounding.normal - root.padding
color: Appearance.colors.colLayer2
color: messageListView.atYBeginning ? Appearance.colors.colLayer2 : Appearance.colors.colLayer2Base
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
RowLayout {
id: statusRowLayout
anchors.centerIn: parent
@@ -13,7 +13,7 @@ Scope {
LazyLoader {
id: barLoader
active: GlobalStates.barOpen && !GlobalStates.screenLocked
active: GlobalStates.barOpen
component: Variants {
model: Quickshell.screens
delegate: PanelWindow { // Bar window
@@ -17,8 +17,9 @@ Singleton {
property string iconsPath: `${Directories.assetsPath}/icons/fluent`
property bool dark: Appearance.m3colors.darkmode
property real backgroundTransparency: 0.16
property real panelBackgroundTransparency: 0.14
readonly property bool transparencyEnabled: Config.options.appearance.transparency.enable
property real backgroundTransparency: transparencyEnabled ? 0.16 : 0
property real panelBackgroundTransparency: transparencyEnabled ? 0.14 : 0
property real panelLayerTransparency: root.dark ? 0.9 : 0.7
property real contentTransparency: root.dark ? 0.87 : 0.5
function applyBackgroundTransparency(col) {
@@ -27,23 +28,22 @@ Singleton {
function applyContentTransparency(col) {
return ColorUtils.applyAlpha(col, 1 - root.contentTransparency)
}
lightColors: QtObject { // TODO: figure out transparency
lightColors: QtObject {
id: lightColors
property color bgPanelFooter: "#EEEEEE"
property color bgPanelBody: "#F2F2F2"
property color bgPanelSeparator: "#E0E0E0"
property color bg0: "#EEEEEE"
property color bg0Border: '#adadad'
property color bg1: "#F7F7F7"
property color bg0Border: '#BEBEBE'
property color bg1Base: "#F7F7F7"
property color bg1: "#F7F7F7"
property color bg1Hover: "#F7F7F7"
property color bg1Active: '#EFEFEF'
property color bg1Border: '#d7d7d7'
property color bg1Border: '#E9E9E9'
property color bg2: "#FBFBFB"
property color bg2Base: "#FBFBFB"
property color bg2Hover: '#ffffff'
property color bg2Active: '#eeeeee'
property color bg2Border: '#cdcdcd'
property color bg2Border: '#E0E0E0'
property color subfg: "#5C5C5C"
property color fg: "#000000"
property color fg1: "#626262"
@@ -58,21 +58,20 @@ Singleton {
}
darkColors: QtObject {
id: darkColors
property color bgPanelFooter: "#1C1C1C"
property color bgPanelBody: '#616161'
property color bgPanelBody: '#242424'
property color bgPanelSeparator: "#191919"
property color bg0: "#1C1C1C"
property color bg0Border: "#404040"
property color bg1Base: "#2C2C2C"
property color bg1: '#9f9f9f'
property color bg1Hover: "#b3b3b3"
property color bg1Active: '#727272'
property color bg1Base: '#2C2C2C'
property color bg1: '#2C2C2C'
property color bg1Hover: "#292929"
property color bg1Active: '#252525'
property color bg1Border: '#bebebe'
property color bg2Base: "#313131"
property color bg2: '#8a8a8a'
property color bg2Hover: '#b1b1b1'
property color bg2Active: '#919191'
property color bg2Border: '#bdbdbd'
property color bg2: '#313131'
property color bg2Hover: '#363636'
property color bg2Active: '#2B2B2B'
property color bg2Border: '#404040'
property color subfg: "#CED1D7"
property color fg: "#FFFFFF"
property color fg1: "#D1D1D1"
@@ -87,38 +86,46 @@ Singleton {
}
colors: QtObject {
id: colors
// Special
property color shadow: ColorUtils.transparentize('#161616', 0.62)
property color ambientShadow: ColorUtils.transparentize("#000000", 0.75)
property color bgPanelFooterBase: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelFooter : root.lightColors.bgPanelFooter, root.panelBackgroundTransparency)
property color bgPanelFooter: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelFooter : root.lightColors.bgPanelFooter, root.panelLayerTransparency)
property color bgPanelBody: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelBody : root.lightColors.bgPanelBody, root.panelLayerTransparency)
property color bgPanelSeparator: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelSeparator : root.lightColors.bgPanelSeparator, root.backgroundTransparency)
property color bg0Opaque: root.dark ? root.darkColors.bg0 : root.lightColors.bg0
property color bg0: ColorUtils.transparentize(bg0Opaque, root.backgroundTransparency)
property color bgPanelFooterBase: ColorUtils.transparentize(root.dark ? root.darkColors.bg0 : root.lightColors.bg0, root.panelBackgroundTransparency)
property color bgPanelFooter: ColorUtils.transparentize(bgPanelFooterBase, root.panelLayerTransparency)
property color bgPanelBodyBase: root.dark ? root.darkColors.bgPanelBody : root.lightColors.bgPanelBody
property color bgPanelBody: ColorUtils.solveOverlayColor(bgPanelFooterBase,bgPanelBodyBase, 1 - root.panelLayerTransparency)
property color bgPanelSeparator: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bgPanelSeparator : root.lightColors.bgPanelSeparator, 1 - root.panelBackgroundTransparency)
// Layer 0
property color bg0Base: root.dark ? root.darkColors.bg0 : root.lightColors.bg0
property color bg0: ColorUtils.transparentize(bg0Base, root.backgroundTransparency)
property color bg0Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg0Border : root.lightColors.bg0Border, root.backgroundTransparency)
property color bg1Base: root.dark ? root.darkColors.bg1Base : root.lightColors.bg1Base
property color bg1: ColorUtils.transparentize(root.dark ? root.darkColors.bg1 : root.lightColors.bg1, root.contentTransparency)
property color bg1Hover: ColorUtils.transparentize(root.dark ? root.darkColors.bg1Hover : root.lightColors.bg1Hover, root.contentTransparency)
property color bg1Active: ColorUtils.transparentize(root.dark ? root.darkColors.bg1Active : root.lightColors.bg1Active, root.contentTransparency)
property color bg1Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg1Border : root.lightColors.bg1Border, root.contentTransparency)
property color bg2Base: root.dark ? root.darkColors.bg2Base : root.lightColors.bg2Base
property color bg2: ColorUtils.transparentize(root.dark ? root.darkColors.bg2 : root.lightColors.bg2, root.contentTransparency)
property color bg2Hover: ColorUtils.transparentize(root.dark ? root.darkColors.bg2Hover : root.lightColors.bg2Hover, root.contentTransparency)
property color bg2Active: ColorUtils.transparentize(root.dark ? root.darkColors.bg2Active : root.lightColors.bg2Active, root.contentTransparency)
property color bg2Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg2Border : root.lightColors.bg2Border, root.contentTransparency)
// Layer 1
property color bg1Base: root.dark ? root.darkColors.bg1 : root.lightColors.bg1
property color bg1: ColorUtils.solveOverlayColor(bg0Base, bg1Base, 1 - root.contentTransparency)
property color bg1Hover: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Hover : root.lightColors.bg1Hover, 1 - root.contentTransparency)
property color bg1Active: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Active : root.lightColors.bg1Active, 1 - root.contentTransparency)
property color bg1Border: ColorUtils.solveOverlayColor(bg0Base, root.dark ? root.darkColors.bg1Border : root.lightColors.bg1Border, 1 - root.contentTransparency)
// Layer 2
property color bg2Base: root.dark ? root.darkColors.bg2 : root.lightColors.bg2
property color bg2: ColorUtils.solveOverlayColor(bgPanelBodyBase, bg2Base, 1 - root.contentTransparency)
property color bg2Hover: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Hover : root.lightColors.bg2Hover, 1 - root.contentTransparency)
property color bg2Active: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Active : root.lightColors.bg2Active, 1 - root.contentTransparency)
property color bg2Border: ColorUtils.solveOverlayColor(bgPanelBodyBase, root.dark ? root.darkColors.bg2Border : root.lightColors.bg2Border, 1 - root.contentTransparency)
// Foreground / Text
property color subfg: root.dark ? root.darkColors.subfg : root.lightColors.subfg
property color fg: root.dark ? root.darkColors.fg : root.lightColors.fg
property color fg1: root.dark ? root.darkColors.fg1 : root.lightColors.fg1
property color inactiveIcon: root.dark ? root.darkColors.inactiveIcon : root.lightColors.inactiveIcon
property color link: root.dark ? root.darkColors.link : root.lightColors.link
// Controls
property color controlBgInactive: root.dark ? root.darkColors.controlBgInactive : root.lightColors.controlBgInactive
property color controlBg: root.dark ? root.darkColors.controlBg : root.lightColors.controlBg
property color controlBgHover: root.dark ? root.darkColors.controlBgHover : root.lightColors.controlBgHover
property color controlFg: root.dark ? root.darkColors.controlFg : root.lightColors.controlFg
property color inputBg: root.dark ? root.darkColors.inputBg : root.lightColors.inputBg
property color link: root.dark ? root.darkColors.link : root.lightColors.link
property color danger: "#C42B1C"
property color dangerActive: "#B62D1F"
property color warning: "#FF9900"
// Accent
property color accent: Appearance.colors.colPrimary
property color accentHover: Appearance.colors.colPrimaryHover
property color accentActive: Appearance.colors.colPrimaryActive
@@ -39,9 +39,10 @@ Button {
}
}
property color fgColor: {
if (!root.enabled) return root.colForegroundDisabled
if (root.checked) return root.colForegroundToggled
if (root.enabled) return root.colForeground
return root.colForegroundDisabled
return root.colForeground
}
property alias horizontalAlignment: buttonText.horizontalAlignment
font {
@@ -76,8 +76,9 @@ Menu {
contentItem: Item {
implicitWidth: menuListView.implicitWidth
implicitHeight: menuListView.implicitHeight
ListView {
WListView {
id: menuListView
interactive: contentHeight > height
anchors {
left: parent.left
right: parent.right
@@ -87,6 +88,7 @@ Menu {
topMargin: root.downDirection ? root.sourceEdgeMargin : root.margins
bottomMargin: root.downDirection ? root.margins : root.sourceEdgeMargin
}
clip: true
implicitHeight: contentHeight
implicitWidth: Array.from({
length: count
@@ -6,6 +6,7 @@ import Quickshell
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
MenuItem {
@@ -14,11 +15,11 @@ MenuItem {
property color colBackground: ColorUtils.transparentize(Looks.colors.bg1)
property color colBackgroundHover: Looks.colors.bg2Hover
property color colBackgroundActive: Looks.colors.bg2Active
property color colBackgroundToggled: Looks.colors.accent
property color colBackgroundToggledHover: Looks.colors.accentHover
property color colBackgroundToggledActive: Looks.colors.accentActive
property color colBackgroundToggled: Looks.colors.bg2Hover
property color colBackgroundToggledHover: Looks.colors.bg2Active
property color colBackgroundToggledActive: Looks.colors.bg2Hover
property color colForeground: Looks.colors.fg
property color colForegroundToggled: Looks.colors.accentFg
property color colForegroundToggled: Looks.colors.fg
property color colForegroundDisabled: ColorUtils.transparentize(Looks.colors.subfg, 0.4)
property color color: {
if (!root.enabled)
@@ -70,27 +71,57 @@ MenuItem {
implicitHeight: Math.max(28, contentItem.implicitHeight) + topInset + bottomInset
implicitWidth: contentItem.implicitWidth + leftInset + rightInset + leftPadding + rightPadding
contentItem: RowLayout {
id: contentLayout
spacing: 12
FluentIcon {
id: buttonIcon
monochrome: true
implicitSize: 20
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter
color: root.fgColor
visible: root.icon.name !== "";
icon: root.icon.name
contentItem: Item {
implicitWidth: contentLayout.implicitWidth
implicitHeight: contentLayout.implicitHeight
RowLayout {
id: contentLayout
anchors.fill: parent
spacing: 12
FluentIcon {
id: buttonIcon
monochrome: true
implicitSize: 20
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter
color: root.fgColor
visible: root.icon.name !== ""
icon: root.icon.name
}
WText {
id: buttonText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.text
horizontalAlignment: Text.AlignLeft
font.pixelSize: Looks.font.pixelSize.large
color: root.fgColor
}
}
WText {
id: buttonText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.text
horizontalAlignment: Text.AlignLeft
font.pixelSize: Looks.font.pixelSize.large
color: root.fgColor
WFadeLoader {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: -root.leftPadding + width
}
shown: root.checked
sourceComponent: Rectangle {
implicitWidth: 3
implicitHeight: 3
radius: width / 2
color: Looks.colors.accent
property bool forceZeroHeight: true
height: forceZeroHeight ? 0 : Math.max(root.down ? 10 : 16, root.background.height - 18 * 2)
Component.onCompleted: {
forceZeroHeight = false;
}
Behavior on height {
animation: Looks.transition.resize.createObject(this)
}
}
}
}
}
@@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Item {
id: root
property real padding: 9
property alias colBackground: background.color
property alias spacing: toolbarLayout.spacing
property alias radius: background.radius
default property alias data: toolbarLayout.data
implicitWidth: background.implicitWidth
implicitHeight: background.implicitHeight
Rectangle {
id: background
anchors.fill: parent
implicitHeight: 50
implicitWidth: toolbarLayout.implicitWidth + root.padding * 2
radius: Looks.radius.large
color: Looks.colors.bg0Base
border.width: 1
border.color: Looks.colors.bg1Border
RowLayout {
id: toolbarLayout
spacing: 4
anchors {
fill: parent
margins: root.padding
}
}
}
}
@@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
WButton {
implicitHeight: 32
radius: Looks.radius.medium
}
@@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
WToolbarButton {
id: root
implicitWidth: height
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: root.icon.name
implicitSize: 18
color: root.fgColor
}
}
}
@@ -0,0 +1,21 @@
import QtQuick
import QtQuick.Controls
import qs.modules.common
TabButton {
id: root
implicitWidth: 38
implicitHeight: 32
padding: 0
background: null
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: root.icon.name
color: root.icon.color
implicitSize: 18
}
}
}
@@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
Rectangle {
Layout.leftMargin: 4
Layout.rightMargin: 4
implicitHeight: 24
implicitWidth: 1
color: Looks.colors.bg0Border
}
@@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import qs.modules.common
import qs.modules.common.functions
TabBar {
id: root
implicitHeight: 32
background: Rectangle {
radius: Looks.radius.medium
color: Looks.colors.bgPanelFooter
border.color: ColorUtils.transparentize(Looks.colors.bg0Border, 0.7)
border.width: 1
// Indicator
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: root.currentIndex * (root.width / root.count)
Behavior on leftMargin {
animation: Looks.transition.resize.createObject(this)
}
}
radius: Looks.radius.medium
color: Looks.colors.bg2Base
border.color: Looks.colors.bg0Border
border.width: 1
width: root.width / root.count
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 1
}
implicitWidth: pressDetector.containsPress ? 16 : 12
implicitHeight: 3
radius: height / 2
color: Looks.colors.accent
}
}
}
MouseArea {
id: pressDetector
z: 9999
anchors.fill: parent
acceptedButtons: Qt.LeftButton
}
}
@@ -7,7 +7,7 @@ import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.waffle.looks
BodyRectangle {
FooterRectangle {
id: root
anchors.fill: parent
implicitHeight: 230
@@ -15,6 +15,9 @@ MouseArea {
required property var notification
property bool expanded: notification.actions.length > 0
property string groupExpandControlMessage: ""
readonly property bool isPopup: notification?.popup ?? false
signal groupExpandToggle
hoverEnabled: true
@@ -56,13 +59,13 @@ MouseArea {
Rectangle {
id: contentItem
width: parent.width
color: Looks.colors.bgPanelBody
radius: Looks.radius.medium
color: root.isPopup ? Looks.colors.bg0 : Looks.colors.bgPanelBody
radius: root.isPopup ? Looks.radius.large : Looks.radius.medium
property real padding: 12
implicitHeight: notificationContent.implicitHeight + padding * 2
implicitWidth: notificationContent.implicitWidth + padding * 2
border.width: 1
border.color: ColorUtils.applyAlpha(Looks.colors.ambientShadow, 0.1)
border.color: root.isPopup ? Looks.colors.bg2Border : Looks.colors.bgPanelSeparator
Behavior on x {
animation: Looks.transition.enter.createObject(this)
@@ -157,9 +160,9 @@ MouseArea {
NotificationHeaderButton {
Layout.rightMargin: 4
opacity: root.containsMouse ? 1 : 0
opacity: (root.containsMouse || root.isPopup) ? 1 : 0
icon.name: "dismiss"
implicitSize: 12
implicitSize: 14
onClicked: root.dismiss()
}
}
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.notificationCenter
Scope {
id: notificationPopup
PanelWindow {
id: root
visible: (Notifications.popupList.length > 0) && !GlobalStates.screenLocked
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? null
WlrLayershell.namespace: "quickshell:notificationPopup"
WlrLayershell.layer: WlrLayer.Overlay
exclusiveZone: 0
anchors {
top: true
right: true
bottom: true
}
mask: Region {
item: listview.contentItem
}
color: "transparent"
implicitWidth: listview.implicitWidth
WListView {
id: listview
anchors {
bottom: parent.bottom
right: parent.right
left: parent.left
}
leftMargin: 16
rightMargin: 16
topMargin: 16
bottomMargin: 16
height: Math.min(contentItem.height + topMargin + bottomMargin, parent.height)
width: parent.width - Appearance.sizes.elevationMargin * 2
implicitWidth: 396
spacing:12
model: ScriptModel {
values: Notifications.popupList
}
delegate: WSingleNotification {
required property var modelData
notification: modelData
width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin
}
}
}
}
@@ -0,0 +1,62 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
Item {
id: root
required property int regionX
required property int regionY
required property int regionWidth
required property int regionHeight
property bool dashed: true
property color borderColor: "#ffffff"
property color overlayColor: ColorUtils.transparentize("#000000", 1)
Component.onCompleted: overlayColor = ColorUtils.transparentize("#000000", 0.4)
Behavior on overlayColor {
ColorAnimation {
duration: 250
easing.type: Easing.InOutQuad
}
}
// Overlay to darken screen
// Base dark overlay around region
Rectangle {
id: darkenOverlay
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: root.regionX - darkenOverlay.border.width
topMargin: root.regionY - darkenOverlay.border.width
}
width: root.regionWidth + darkenOverlay.border.width * 2
height: root.regionHeight + darkenOverlay.border.width * 2
color: "transparent"
border.color: root.overlayColor
border.width: Math.max(root.width, root.height)
}
// Selection border
DashedBorder {
id: border
z: 2
visible: root.regionWidth > 0 && root.regionHeight > 0
anchors {
left: parent.left
top: parent.top
leftMargin: Math.round(root.regionX - borderWidth)
topMargin: Math.round(root.regionY - borderWidth)
}
width: Math.round(root.regionWidth + borderWidth * 2)
height: Math.round(root.regionHeight + borderWidth * 2)
color: root.borderColor
dashLength: 4
gapLength: root.dashed ? 3 : 0
borderWidth: 1
}
}
@@ -0,0 +1,375 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.synchronizer
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.utils
import qs.modules.common.widgets
import qs.modules.waffle.looks
PanelWindow {
id: root
enum MediaType {
Image,
Video
}
enum ImageAction {
Copy,
Menu,
CharRecognition,
Search
}
enum VideoAction {
Record,
RecordWithSound
}
enum SelectionMode {
Rect,
Window
}
signal closed
function close() {
root.closed();
}
property var mediaType: WRegionSelectionPanel.MediaType.Image
property var imageAction: WRegionSelectionPanel.ImageAction.Copy
property var selectionMode: WRegionSelectionPanel.SelectionMode.Rect
visible: false
color: "transparent"
WlrLayershell.namespace: "quickshell:regionSelector"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
exclusionMode: ExclusionMode.Ignore
anchors {
left: true
right: true
top: true
bottom: true
}
// Hyprland stuff
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
readonly property real monitorScale: hyprlandMonitor.scale
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
if (a.floating === b.floating)
return 0;
return a.floating ? -1 : 1;
})
property string screenshotDir: Directories.screenshotTemp
property string screenshotPath: `${root.screenshotDir}/image-${screen.name}`
TempScreenshotProcess {
id: screenshotProc
running: true
screen: root.screen
screenshotDir: root.screenshotDir
screenshotPath: root.screenshotPath
onExited: (exitCode, exitStatus) => {
root.preparationDone = true;
}
}
property bool preparationDone: false
onPreparationDoneChanged: {
if (!preparationDone)
return;
root.visible = true;
}
function getScreenshotAction() {
switch (root.mediaType) {
case WRegionSelectionPanel.MediaType.Image:
switch (root.imageAction) {
case WRegionSelectionPanel.ImageAction.Copy:
return ScreenshotAction.Action.Copy;
case WRegionSelectionPanel.ImageAction.Menu:
return ScreenshotAction.Action.Edit;
case WRegionSelectionPanel.ImageAction.CharRecognition:
return ScreenshotAction.Action.CharRecognition;
case WRegionSelectionPanel.ImageAction.Search:
return ScreenshotAction.Action.Search;
default:
return ScreenshotAction.Action.Copy;
}
break;
case WRegionSelectionPanel.MediaType.Video:
switch (root.videoAction) {
case WRegionSelectionPanel.VideoAction.Record:
return ScreenshotAction.Action.Record;
case WRegionSelectionPanel.VideoAction.RecordWithSound:
return ScreenshotAction.Action.RecordWithSound;
}
}
}
Process {
id: snipProc
}
ScreencopyView {
id: screencopyView
anchors.fill: parent
live: false
captureSource: root.screen
focus: root.visible
Keys.onPressed: event => { // Esc to close
if (event.key === Qt.Key_Escape) {
root.close();
} else if (event.key === Qt.Key_E && event.modifiers & Qt.ControlModifier) {
if (root.imageAction === WRegionSelectionPanel.ImageAction.Menu) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.imageAction = WRegionSelectionPanel.ImageAction.Menu;
}
}
}
DragManager {
id: dragArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.CrossCursor
property bool isWindowSelection: root.selectionMode === WRegionSelectionPanel.SelectionMode.Window
property var hoveredWindow: root.windows.find(w => {
const inCurrentWorkspace = w.workspace.id === HyprlandData.activeWorkspace.id;
const withinXRange = w.at[0] <= dragArea.mouseX && dragArea.mouseX <= w.at[0] + w.size[0];
const withinYRange = w.at[1] <= dragArea.mouseY && dragArea.mouseY <= w.at[1] + w.size[1];
return inCurrentWorkspace && withinXRange && withinYRange;
})
property int winPadding: 1
property int selectionX: isWindowSelection ? ((hoveredWindow?.at[0] ?? 0) - winPadding) : regionTopLeftX
property int selectionY: isWindowSelection ? ((hoveredWindow?.at[1] ?? 0) - winPadding) : regionTopLeftY
property int selectionWidth: isWindowSelection ? ((hoveredWindow?.size[0] ?? 0) + winPadding * 2) : regionWidth
property int selectionHeight: isWindowSelection ? ((hoveredWindow?.size[1] ?? 0) + winPadding * 2) : regionHeight
onDragReleased: (diffX, diffY) => {
if (selectionWidth === 0 || selectionHeight === 0) {
return;
}
const screenshotDir = Config.options.screenSnip.savePath !== "" ? Config.options.screenSnip.savePath : "";
const screenshotAction = root.getScreenshotAction();
const command = ScreenshotAction.getCommand(dragArea.selectionX * root.monitorScale //
, dragArea.selectionY * root.monitorScale //
, dragArea.selectionWidth * root.monitorScale//
, dragArea.selectionHeight * root.monitorScale //
, root.screenshotPath //
, screenshotAction //
, screenshotDir); // yo wtf is this formatting qmlls do be funnie
snipProc.command = command;
// Image post-processing
snipProc.startDetached();
root.close();
}
WRectangularSelection {
id: rectangularSelection
anchors.fill: parent
regionX: dragArea.selectionX
regionY: dragArea.selectionY
regionWidth: dragArea.selectionWidth
regionHeight: dragArea.selectionHeight
dashed: root.selectionMode === WRegionSelectionPanel.SelectionMode.Rect
}
RegionSelectionOptionsToolbar {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 12
}
}
}
}
component RegionSelectionOptionsToolbar: WToolbar {
// Image/video
WToolbarTabBar {
currentIndex: switch (root.mediaType) {
case WRegionSelectionPanel.MediaType.Image:
return 0;
case WRegionSelectionPanel.MediaType.Video:
return 1;
default:
return 0;
}
WToolbarIconTabButton {
icon.name: "camera"
icon.color: Looks.colors.fg
}
WToolbarIconTabButton {
icon.name: "video"
icon.color: Looks.colors.fg
}
onCurrentIndexChanged: {
switch (currentIndex) {
case 0:
root.mediaType = WRegionSelectionPanel.MediaType.Image;
break;
case 1:
root.mediaType = WRegionSelectionPanel.MediaType.Video;
break;
}
}
WToolTip {
text: Translation.tr("Snip")
}
}
// Selection type
WToolbarButton {
id: selectionTypeBtn
implicitWidth: selectionTypeBtnRow.implicitWidth + 11 * 2
leftPadding: 11
rightPadding: 11
onClicked: {
selectionTypeMenu.visible = !selectionTypeMenu.visible;
}
contentItem: Row {
id: selectionTypeBtnRow
spacing: 4
FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: switch (root.selectionMode) {
case WRegionSelectionPanel.SelectionMode.Rect:
return "crop";
case WRegionSelectionPanel.SelectionMode.Window:
return "calendar-add";
default:
return "crop";
}
implicitSize: 18
}
FluentIcon {
anchors {
top: parent.top
topMargin: (parent.height - height) / 2 + (selectionTypeBtn.down ? 2 : 0)
Behavior on topMargin {
animation: Looks.transition.enter.createObject(this)
}
}
icon: "chevron-down"
implicitSize: 12
}
}
WMenu {
id: selectionTypeMenu
onClosed: screencopyView.focus = true
x: -margins
y: -margins - (selectionTypeBtn.parent.height - selectionTypeBtn.height) - 16
topMargin: -6
height: implicitHeight + sourceEdgeMargin
color: Looks.colors.bg1Base
Action {
icon.name: "crop"
text: Translation.tr("Rectangle")
checked: root.selectionMode === WRegionSelectionPanel.SelectionMode.Rect
onTriggered: {
root.selectionMode = WRegionSelectionPanel.SelectionMode.Rect;
}
}
Action {
icon.name: "calendar-add"
text: Translation.tr("Window")
checked: root.selectionMode === WRegionSelectionPanel.SelectionMode.Window
onTriggered: {
root.selectionMode = WRegionSelectionPanel.SelectionMode.Window;
}
}
}
WToolTip {
text: Translation.tr("Snipping area")
}
}
// Markup
WToolbarIconButton {
icon.name: "image-edit"
enabled: root.mediaType === WRegionSelectionPanel.MediaType.Image
checked: root.imageAction === WRegionSelectionPanel.ImageAction.Menu
onClicked: {
if (root.imageAction === WRegionSelectionPanel.ImageAction.Menu) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.imageAction = WRegionSelectionPanel.ImageAction.Menu;
}
}
WToolTip {
text: Translation.tr("Quick markup (Ctrl+E)")
}
}
WToolbarSeparator {}
// Tools
WToolbarIconButton {
icon.name: "search-visual"
checked: root.imageAction === WRegionSelectionPanel.ImageAction.Search
onClicked: {
if (root.imageAction === WRegionSelectionPanel.ImageAction.Search && root.mediaType === WRegionSelectionPanel.MediaType.Image) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.mediaType = WRegionSelectionPanel.MediaType.Image;
root.imageAction = WRegionSelectionPanel.ImageAction.Search;
}
}
WToolTip {
text: Translation.tr("Image search")
}
}
WToolbarIconButton {
icon.name: "eyedropper"
onClicked: {
Quickshell.execDetached(["bash", "-c", "sleep 0.2; hyprpicker -a"]);
root.closed();
}
WToolTip {
text: Translation.tr("Color picker")
}
}
WToolbarIconButton {
icon.name: "scan-text"
checked: root.imageAction === WRegionSelectionPanel.ImageAction.CharRecognition
onClicked: {
if (root.imageAction === WRegionSelectionPanel.ImageAction.CharRecognition && root.mediaType === WRegionSelectionPanel.MediaType.Image) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.mediaType = WRegionSelectionPanel.MediaType.Image;
root.imageAction = WRegionSelectionPanel.ImageAction.CharRecognition;
}
}
WToolTip {
text: Translation.tr("Text extractor")
}
}
WToolbarSeparator {}
WToolbarIconButton {
icon.name: "dismiss"
onClicked: root.close()
WToolTip {
text: Translation.tr("Close (Esc)")
}
}
}
}
@@ -0,0 +1,106 @@
pragma ComponentBehavior: Bound
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Hyprland
Scope {
id: root
function dismiss() {
GlobalStates.regionSelectorOpen = false;
}
Loader {
id: regionSelectorLoader
active: GlobalStates.regionSelectorOpen
sourceComponent: WRegionSelectionPanel {
onClosed: root.dismiss()
}
}
function screenshot() {
GlobalStates.regionSelectorOpen = true;
}
function ocr() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Image;
regionSelectorLoader.item.imageAction = WRegionSelectionPanel.ImageAction.CharRecognition;
}
function record() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Video;
regionSelectorLoader.item.videoAction = WRegionSelectionPanel.VideoAction.Record;
}
function recordWithSound() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Video;
regionSelectorLoader.item.videoAction = WRegionSelectionPanel.VideoAction.RecordWithSound;
}
function search() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Image;
regionSelectorLoader.item.imageAction = WRegionSelectionPanel.ImageAction.Search;
}
IpcHandler {
target: "region"
function screenshot() {
root.screenshot();
}
function ocr() {
root.ocr();
}
function record() {
root.record();
}
function recordWithSound() {
root.recordWithSound();
}
function search() {
root.search();
}
}
GlobalShortcut {
name: "regionScreenshot"
description: "Takes a screenshot of the selected region"
onPressed: root.screenshot()
}
GlobalShortcut {
name: "regionSearch"
description: "Searches the selected region"
onPressed: root.search()
}
GlobalShortcut {
name: "regionOcr"
description: "Recognizes text in the selected region"
onPressed: root.ocr()
}
GlobalShortcut {
name: "regionRecord"
description: "Records the selected region"
onPressed: root.record()
}
GlobalShortcut {
name: "regionRecordWithSound"
description: "Records the selected region with sound"
onPressed: root.recordWithSound()
}
}
@@ -44,7 +44,7 @@ WChoiceButton {
Layout.fillHeight: true
horizontalPadding: 10
verticalPadding: 11
implicitHeight: root.firstEntry ? 62 : 36
implicitHeight: Math.max(root.firstEntry ? 62 : 36, entryContentRow.implicitHeight + 8 * 2)
implicitWidth: entryContentRow.implicitWidth + leftPadding + rightPadding
topRightRadius: 0
bottomRightRadius: 0
@@ -54,6 +54,7 @@ WChoiceButton {
id: entryContentRow
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
spacing: 8
@@ -102,6 +103,7 @@ WChoiceButton {
text: root.entry.name
font.pixelSize: Looks.font.pixelSize.large
maximumLineCount: 2
elide: Text.ElideRight
}
WText {
@@ -109,6 +111,7 @@ WChoiceButton {
visible: root.firstEntry
text: root.entry.type
color: Looks.colors.accentUnfocused
elide: Text.ElideRight
}
}
@@ -15,6 +15,7 @@ RowLayout {
id: root
property int maxResultsPerCategory: 4
property int resultLimit: 20
property StartMenuContext context
property int currentIndex: context.currentIndex
onCurrentIndexChanged: {
@@ -99,21 +100,33 @@ RowLayout {
// Collect max 4 per category
var categorizedResults = [];
categories.forEach(category => {
let categoriesArray = Array.from(categories);
let totalCount = 0;
for (let c = 0; c < categoriesArray.length; c++) {
let category = categoriesArray[c];
let count = 0;
for (let i = 0; i < allResults.length; i++) {
if (allResults[i].type === category) {
if (totalCount >= root.resultLimit) {
break;
}
const entry = allResults[i];
const tweakedEntry = searchResultComp.createObject(null, Object.assign({}, entry));
tweakedEntry.category = categorizedResults.length === 0 ? Translation.tr("Best match") : entry.type
tweakedEntry.category = categorizedResults.length === 0 ? Translation.tr("Best match") : entry.type;
categorizedResults.push(tweakedEntry); // Section header
count++;
totalCount++;
if (count >= root.maxResultsPerCategory) {
break;
}
}
}
});
if (totalCount >= root.resultLimit) {
break;
}
}
// print(JSON.stringify(categorizedResults, null, 2));
return categorizedResults;
}
@@ -28,13 +28,13 @@ GridLayout {
}), aggAppCatComp.createObject(null, {
name: Translation.tr("Creativity"),
categories: ["AudioVideo", "Graphics"]
}), aggAppCatComp.createObject(null, {
name: Translation.tr("Other"),
categories: ["Game"]
}), aggAppCatComp.createObject(null, {
name: Translation.tr("System"),
categories: ["Settings", "System"]
})
}), aggAppCatComp.createObject(null, {
name: Translation.tr("Other"),
categories: ["Game"]
}),
]
Repeater {