diff options
| author | littleblack111 <littleblack11111@gmail.com> | 2026-09-02 18:26:45 +0800 |
|---|---|---|
| committer | littleblack111 <littleblack11111@gmail.com> | 2026-09-02 18:26:45 +0800 |
| commit | f7a43077fe4e139dd7e3f46f448e0951740ec148 (patch) | |
| tree | f6a68248fe42bd899e99049bab7fdb7e5d26ea60 | |
| parent | 2f7576faf493b4667b02dcb60ddbf3aa0a9f654b (diff) | |
fix: qmlfmt
IListView: animation
56 files changed, 4726 insertions, 4832 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b605332..1fe2990 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,7 +170,7 @@ jobs: prettier --check "utils/**/*.js" - name: Check QML/JS Format - run: find . \( -name "*.qml" -o -name "*.js" \) -exec /usr/lib/qt6/bin/qmlformat -n {} + + run: find . \( -name "*.qml" -o -name "*.js" \) -exec /usr/lib/qt6/bin/qmlformat {} + - name: Lint Shell run: shellcheck utils/*.sh diff --git a/.qmlformat.ini b/.qmlformat.ini new file mode 100644 index 0000000..525e0b6 --- /dev/null +++ b/.qmlformat.ini @@ -0,0 +1,12 @@ +[General] +FunctionsSpacing=false +GroupAttributesTogether=false +IndentWidth=4 +MaxColumnWidth=-1 +NewlineType=native +NormalizeOrder=false +ObjectsSpacing=false +SemicolonRule=always +SingleLineEmptyObjects=false +SortImports=false +UseTabs=true diff --git a/bar/ActiveWindow.qml b/bar/ActiveWindow.qml index 17d2322..0a6a547 100644 --- a/bar/ActiveWindow.qml +++ b/bar/ActiveWindow.qml @@ -10,113 +10,113 @@ import qs.config // TODO use a item wrapping loader for opacity Behavior IRect { - id: root - // hyprland toplevel isn't shown immediately... - property var toplevel: ToplevelManager.activeToplevel - property var icon: Quickshell.iconPath(AppSearch.guessIcon(toplevel?.appId), "image-missing") - - property var activated: toplevel?.activated || false - - implicitWidth: activated ? rowLayout.implicitWidth + General.rectMargin * 4 : 0 - implicitHeight: rowLayout.implicitHeight + General.rectMargin * 2 + Bar.topMargin * 2 - anchors.centerIn: parent - - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Bar.bgTransparency) - // according to docs, null could happen but doesn't seem to happen in practice so.. - // hypr toplevel.activated is always true and doesn't change either so.. - opacity: activated ? 1 : 0 - - bottomLeftRadius: Bar.moduleRadius - bottomRightRadius: Bar.moduleRadius - - function strip(s) { - var out = s; - Bar.windowStrip.forEach(function (w) { - out = out?.replace(new RegExp(w, "g"), ""); - }); - return out?.trim() || s; - } - - RowLayout { - id: rowLayout - anchors.centerIn: parent - spacing: Bar.resourceIconTextSpacing - width: Math.min(implicitWidth, root.width) - - IconImage { - id: icon - source: root.icon - implicitSize: General.appIconSize - - // sync animation - opacity: root.activated ? 1 : 0 - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: childrenRect.width - IText { - anchors.verticalCenter: parent.verticalCenter - width: Math.min(implicitWidth, rowLayout.width - icon.implicitWidth) - animate: true - clip: true - elide: Text.ElideRight - font.pixelSize: General.fontSize - text: strip(root?.toplevel?.title) || "" - - // sync animation - opacity: root.activated ? 1 : 0 - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - } - } - } - } - } - - onIconChanged: fadeIcon.start() - - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - } - } - - Behavior on width { - ISpringAnimation { - spring: General.springAnimationSpring * 2 - damping: General.springAnimationDamping * 1.3 - } - } - - SequentialAnimation { - id: fadeIcon - running: false - - PropertyAnimation { - target: icon - property: "opacity" - to: 0 - duration: General.animationDuration / 4 - } - ScriptAction { - script: icon.source = root.icon - } - PropertyAnimation { - target: icon - property: "opacity" - to: 1 - duration: General.animationDuration / 4 - } - ScriptAction { - script: icon.source = root.icon - } - } + id: root + // hyprland toplevel isn't shown immediately... + property var toplevel: ToplevelManager.activeToplevel + property var icon: Quickshell.iconPath(AppSearch.guessIcon(toplevel?.appId), "image-missing") + + property var activated: toplevel?.activated || false + + implicitWidth: activated ? rowLayout.implicitWidth + General.rectMargin * 4 : 0 + implicitHeight: rowLayout.implicitHeight + General.rectMargin * 2 + Bar.topMargin * 2 + anchors.centerIn: parent + + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Bar.bgTransparency) + // according to docs, null could happen but doesn't seem to happen in practice so.. + // hypr toplevel.activated is always true and doesn't change either so.. + opacity: activated ? 1 : 0 + + bottomLeftRadius: Bar.moduleRadius + bottomRightRadius: Bar.moduleRadius + + function strip(s) { + var out = s; + Bar.windowStrip.forEach(function (w) { + out = out?.replace(new RegExp(w, "g"), ""); + }); + return out?.trim() || s; + } + + RowLayout { + id: rowLayout + anchors.centerIn: parent + spacing: Bar.resourceIconTextSpacing + width: Math.min(implicitWidth, root.width) + + IconImage { + id: icon + source: root.icon + implicitSize: General.appIconSize + + // sync animation + opacity: root.activated ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + implicitWidth: childrenRect.width + IText { + anchors.verticalCenter: parent.verticalCenter + width: Math.min(implicitWidth, rowLayout.width - icon.implicitWidth) + animate: true + clip: true + elide: Text.ElideRight + font.pixelSize: General.fontSize + text: strip(root?.toplevel?.title) || "" + + // sync animation + opacity: root.activated ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + } + } + } + } + } + + onIconChanged: fadeIcon.start() + + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + } + } + + Behavior on width { + ISpringAnimation { + spring: General.springAnimationSpring * 2 + damping: General.springAnimationDamping * 1.3 + } + } + + SequentialAnimation { + id: fadeIcon + running: false + + PropertyAnimation { + target: icon + property: "opacity" + to: 0 + duration: General.animationDuration / 4 + } + ScriptAction { + script: icon.source = root.icon + } + PropertyAnimation { + target: icon + property: "opacity" + to: 1 + duration: General.animationDuration / 4 + } + ScriptAction { + script: icon.source = root.icon + } + } } diff --git a/bar/Bar.qml b/bar/Bar.qml index 6b762b1..2ad13d1 100644 --- a/bar/Bar.qml +++ b/bar/Bar.qml @@ -5,143 +5,143 @@ import qs.components import qs.config Scope { - id: root - - Variants { - model: Quickshell.screens - - IWindow { - id: barWindow - name: "quickshell::bar" - - anchors { - top: true - left: true - right: true - } - implicitHeight: child.implicitHeight + Bar.topMargin * 2 - - IRect { - id: child - radius: Bar.borderRadius - implicitHeight: Bar.height - implicitWidth: parent.width - - anchors { - left: parent.left - right: parent.mid - fill: parent - top: parent.top - } - - color: "transparent" - - IRect { - id: leftRow - anchors { - left: parent.left - } - width: leftInnerRow.width + leftInnerRow.anchors.leftMargin - leftSpacer.width + Bar.leftMargin - height: leftInnerRow.height - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Bar.bgTransparency) - bottomRightRadius: Bar.moduleRadius - - Behavior on color { - ColorAnimation { - duration: General.animationDuration - easing.type: Easing.InOutQuad - } - } - RowLayout { - id: leftInnerRow - height: child.height - spacing: Bar.leftModuleSpacing - anchors { - left: parent.left - leftMargin: Bar.moduleLeftMargin + Bar.leftMargin - } - - Workspace { - id: workspace - // screen: barWindow.screen // multi monitor - // Layout.minimumWidth: 400 // FIXME - } - Mpris { - id: mpris - } - Item { - id: leftSpacer - Layout.fillWidth: true - Layout.fillHeight: true - } - } - } - - Item { - id: centerRow - anchors.centerIn: parent - - ActiveWindow { - id: activeWindow - width: Math.min(implicitWidth, child.width - Math.max(leftRow.width, rightRow.width) * 2 - Bar.leftMargin - Bar.rightMargin) // we don't do child.width - leftRow.width - rightRow.width because left and right width isn't symetric, and we're centered meaning we will colide into the larger width side - } - } - - IRect { - id: rightRow - anchors.right: parent.right - width: rightInnerRow.width + rightInnerRow.anchors.rightMargin - rightSpacer.width - height: rightInnerRow.height - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Bar.bgTransparency) - bottomLeftRadius: Bar.moduleRadius - - RowLayout { - id: rightInnerRow - anchors { - right: parent.right - rightMargin: Bar.moduleRightMargin - } - layoutDirection: Qt.RightToLeft - height: child.height - spacing: Bar.rightModuleSpacing - - Power { - id: power - } - // Tray { - // id: tray - // } - TimeDate {} - Network {} - // Temp { - // id: temp - // } - // Gpu { - // id: gpu - // } - // Memory { - // id: memory - // } - // Cpu { - // id: cpu - // } - Resources {} - - Item { - id: rightSpacer - Layout.fillWidth: true - Layout.fillHeight: true - } - } - - Behavior on color { - ColorAnimation { - duration: General.animationDuration - easing.type: Easing.InOutQuad - } - } - } - } - } - } + id: root + + Variants { + model: Quickshell.screens + + IWindow { + id: barWindow + name: "quickshell::bar" + + anchors { + top: true + left: true + right: true + } + implicitHeight: child.implicitHeight + Bar.topMargin * 2 + + IRect { + id: child + radius: Bar.borderRadius + implicitHeight: Bar.height + implicitWidth: parent.width + + anchors { + left: parent.left + right: parent.mid + fill: parent + top: parent.top + } + + color: "transparent" + + IRect { + id: leftRow + anchors { + left: parent.left + } + width: leftInnerRow.width + leftInnerRow.anchors.leftMargin - leftSpacer.width + Bar.leftMargin + height: leftInnerRow.height + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Bar.bgTransparency) + bottomRightRadius: Bar.moduleRadius + + Behavior on color { + ColorAnimation { + duration: General.animationDuration + easing.type: Easing.InOutQuad + } + } + RowLayout { + id: leftInnerRow + height: child.height + spacing: Bar.leftModuleSpacing + anchors { + left: parent.left + leftMargin: Bar.moduleLeftMargin + Bar.leftMargin + } + + Workspace { + id: workspace + // screen: barWindow.screen // multi monitor + // Layout.minimumWidth: 400 // FIXME + } + Mpris { + id: mpris + } + Item { + id: leftSpacer + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + + Item { + id: centerRow + anchors.centerIn: parent + + ActiveWindow { + id: activeWindow + width: Math.min(implicitWidth, child.width - Math.max(leftRow.width, rightRow.width) * 2 - Bar.leftMargin - Bar.rightMargin) // we don't do child.width - leftRow.width - rightRow.width because left and right width isn't symetric, and we're centered meaning we will colide into the larger width side + } + } + + IRect { + id: rightRow + anchors.right: parent.right + width: rightInnerRow.width + rightInnerRow.anchors.rightMargin - rightSpacer.width + height: rightInnerRow.height + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Bar.bgTransparency) + bottomLeftRadius: Bar.moduleRadius + + RowLayout { + id: rightInnerRow + anchors { + right: parent.right + rightMargin: Bar.moduleRightMargin + } + layoutDirection: Qt.RightToLeft + height: child.height + spacing: Bar.rightModuleSpacing + + Power { + id: power + } + // Tray { + // id: tray + // } + TimeDate {} + Network {} + // Temp { + // id: temp + // } + // Gpu { + // id: gpu + // } + // Memory { + // id: memory + // } + // Cpu { + // id: cpu + // } + Resources {} + + Item { + id: rightSpacer + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + Behavior on color { + ColorAnimation { + duration: General.animationDuration + easing.type: Easing.InOutQuad + } + } + } + } + } + } } diff --git a/bar/Mpris.qml b/bar/Mpris.qml index 75f7e2d..4bed0fe 100644 --- a/bar/Mpris.qml +++ b/bar/Mpris.qml @@ -13,187 +13,187 @@ import qs.components import "../utils/string_utils.js" as StringUtils Item { - id: root - readonly property MprisPlayer activePlayer: Services.Mpris.activePlayer - readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) - - implicitWidth: activePlayer ? rect.width - General.rectMargin / 1.2 : 0 - height: Bar.height + General.rectMargin / 1.5 - opacity: activePlayer ? 1 : 0 - - IRect { - id: rect - anchors.verticalCenter: parent.verticalCenter - implicitWidth: (layout.width + General.rectMargin * 4) * (mouseArea.containsMouse ? Bar.hoverScale : 1.0) - height: parent.height - General.rectMargin - radius: Style.rounding.smaller - color: "transparent" - - // FIXME: at the start, the progress bar's radius will make it go out of bounds. use OpacityMask or something - Slider { - id: pBar - anchors.fill: parent - - handle: Item - from: 0 - to: activePlayer?.length || 0 - value: activePlayer?.position || 0 - background: IRect { - anchors.fill: parent - color: Colors.accent - radius: Style.rounding.smaller - } - contentItem: Item { - IRect { - width: pBar.visualPosition * parent.width - height: parent.height - color: Qt.rgba(Colors.accentAlt.r, Colors.accentAlt.g, Colors.accentAlt.b, General.accentTransparency * 1.5) - radius: Style.rounding.smaller - - Behavior on width { - NumberAnimation { - duration: General.animationDuration / 2 - } - } - } - } - - // onMoved: { - // if (Math.round(root.activePlayer.position) !== Math.round(value)) { - // root.activePlayer.position = value; - // } - // } - - // cannot remove and replace with modern non stealing handlers cuz otherwise slider will steal from us - MouseArea { - id: mouseArea - anchors.fill: parent - acceptedButtons: Qt.MiddleButton | Qt.RightButton | Qt.LeftButton - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onClicked: mouse => { - if (mouse.button === Qt.MiddleButton) - root.activePlayer?.togglePlaying(); - else if (mouse.button === Qt.RightButton) - root.activePlayer?.next(); - } - onWheel: mouse => { - root.activePlayer?.seek(-mouse.angleDelta.y * Bar.mediaScrollScale); - } - } - } - - RowLayout { - id: layout - - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - spacing: Bar.resourceIconTextSpacing / 1.5 - - Behavior on x { - ISpringAnimation { - spring: General.springAnimationSpring * 2 - damping: General.springAnimationDamping * 1.3 - } - } - - Item { - Layout.preferredWidth: childrenRect.width - Layout.preferredHeight: childrenRect.height - width: General.appIconSize - height: General.appIconSize - - opacity: activePlayer?.playbackState === MprisPlaybackState.Playing ? 1 : Bar.mediaPausedOpacity - - DropShadow { - anchors.fill: image - source: image - opacity: activePlayer?.playbackState === MprisPlaybackState.Playing ? 1 : Bar.mediaPausedOpacity - radius: 6 - color: Colors.background3 - - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - } - - ClippingWrapperRectangle { - id: image - anchors.fill: parent - radius: 6 - color: "transparent" - child: Image { - id: albumArt - // IconImage makes it less crisp and blurry - anchors.fill: parent - source: activePlayer?.trackArtUrl || Quickshell.iconPath(Services.AppSearch.guessIcon(activePlayer?.desktopEntry), "image-missing") - antialiasing: true - asynchronous: true - ColorOverlay { - property color rawColor: activePlayer?.playbackState === MprisPlaybackState.Playing ? Colors.background1 : Colors.background2 - anchors.fill: albumArt - source: albumArt - color: Qt.rgba(rawColor.r, rawColor.g, rawColor.b, General.accentTransparency) - - Behavior on color { - ColorAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - } - } - } - - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - } - IText { - animate: true - color: activePlayer?.playbackState === MprisPlaybackState.Playing ? Qt.rgba(Colors.foreground1.r, Colors.foreground1.g, Colors.foreground1.b, Colors.foreground1.a) : Qt.rgba(Colors.foreground2.r, Colors.foreground2.g, Colors.foreground2.b, Bar.mediaPausedOpacity) - text: root.cleanedTitle - - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - - Behavior on color { - ColorAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - } - } - } - - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - } - } - - Behavior on width { - ISpringAnimation { - spring: General.springAnimationSpring * 2 - damping: General.springAnimationDamping * 1.3 - } - } - - Behavior on implicitWidth { - ISpringAnimation { - spring: General.springAnimationSpring * 2 - damping: General.springAnimationDamping * 1.3 - } - } + id: root + readonly property MprisPlayer activePlayer: Services.Mpris.activePlayer + readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) + + implicitWidth: activePlayer ? rect.width - General.rectMargin / 1.2 : 0 + height: Bar.height + General.rectMargin / 1.5 + opacity: activePlayer ? 1 : 0 + + IRect { + id: rect + anchors.verticalCenter: parent.verticalCenter + implicitWidth: (layout.width + General.rectMargin * 4) * (mouseArea.containsMouse ? Bar.hoverScale : 1.0) + height: parent.height - General.rectMargin + radius: Style.rounding.smaller + color: "transparent" + + // FIXME: at the start, the progress bar's radius will make it go out of bounds. use OpacityMask or something + Slider { + id: pBar + anchors.fill: parent + + handle: Item + from: 0 + to: activePlayer?.length || 0 + value: activePlayer?.position || 0 + background: IRect { + anchors.fill: parent + color: Colors.accent + radius: Style.rounding.smaller + } + contentItem: Item { + IRect { + width: pBar.visualPosition * parent.width + height: parent.height + color: Qt.rgba(Colors.accentAlt.r, Colors.accentAlt.g, Colors.accentAlt.b, General.accentTransparency * 1.5) + radius: Style.rounding.smaller + + Behavior on width { + NumberAnimation { + duration: General.animationDuration / 2 + } + } + } + } + + // onMoved: { + // if (Math.round(root.activePlayer.position) !== Math.round(value)) { + // root.activePlayer.position = value; + // } + // } + + // cannot remove and replace with modern non stealing handlers cuz otherwise slider will steal from us + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.MiddleButton | Qt.RightButton | Qt.LeftButton + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: mouse => { + if (mouse.button === Qt.MiddleButton) + root.activePlayer?.togglePlaying(); + else if (mouse.button === Qt.RightButton) + root.activePlayer?.next(); + } + onWheel: mouse => { + root.activePlayer?.seek(-mouse.angleDelta.y * Bar.mediaScrollScale); + } + } + } + + RowLayout { + id: layout + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + spacing: Bar.resourceIconTextSpacing / 1.5 + + Behavior on x { + ISpringAnimation { + spring: General.springAnimationSpring * 2 + damping: General.springAnimationDamping * 1.3 + } + } + + Item { + Layout.preferredWidth: childrenRect.width + Layout.preferredHeight: childrenRect.height + width: General.appIconSize + height: General.appIconSize + + opacity: activePlayer?.playbackState === MprisPlaybackState.Playing ? 1 : Bar.mediaPausedOpacity + + DropShadow { + anchors.fill: image + source: image + opacity: activePlayer?.playbackState === MprisPlaybackState.Playing ? 1 : Bar.mediaPausedOpacity + radius: 6 + color: Colors.background3 + + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + } + + ClippingWrapperRectangle { + id: image + anchors.fill: parent + radius: 6 + color: "transparent" + child: Image { + id: albumArt + // IconImage makes it less crisp and blurry + anchors.fill: parent + source: activePlayer?.trackArtUrl || Quickshell.iconPath(Services.AppSearch.guessIcon(activePlayer?.desktopEntry), "image-missing") + antialiasing: true + asynchronous: true + ColorOverlay { + property color rawColor: activePlayer?.playbackState === MprisPlaybackState.Playing ? Colors.background1 : Colors.background2 + anchors.fill: albumArt + source: albumArt + color: Qt.rgba(rawColor.r, rawColor.g, rawColor.b, General.accentTransparency) + + Behavior on color { + ColorAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + } + IText { + animate: true + color: activePlayer?.playbackState === MprisPlaybackState.Playing ? Qt.rgba(Colors.foreground1.r, Colors.foreground1.g, Colors.foreground1.b, Colors.foreground1.a) : Qt.rgba(Colors.foreground2.r, Colors.foreground2.g, Colors.foreground2.b, Bar.mediaPausedOpacity) + text: root.cleanedTitle + + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + + Behavior on color { + ColorAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + } + } + + Behavior on width { + ISpringAnimation { + spring: General.springAnimationSpring * 2 + damping: General.springAnimationDamping * 1.3 + } + } + + Behavior on implicitWidth { + ISpringAnimation { + spring: General.springAnimationSpring * 2 + damping: General.springAnimationDamping * 1.3 + } + } } diff --git a/bar/Network.qml b/bar/Network.qml index a343977..940f583 100644 --- a/bar/Network.qml +++ b/bar/Network.qml @@ -6,116 +6,116 @@ import qs.components import qs.config Item { - id: root - property int down: Services.NetworkUsage.down - property string downUnit: Services.NetworkUsage.downUnit - property int up: Services.NetworkUsage.up - property string upUnit: Services.NetworkUsage.upUnit + id: root + property int down: Services.NetworkUsage.down + property string downUnit: Services.NetworkUsage.downUnit + property int up: Services.NetworkUsage.up + property string upUnit: Services.NetworkUsage.upUnit - property int significantUsage: 30 - property string usageUnit: "M" + property int significantUsage: 30 + property string usageUnit: "M" - property bool isCollapsed: true + property bool isCollapsed: true - implicitWidth: container.implicitWidth - implicitHeight: container.implicitHeight - General.rectMargin + implicitWidth: container.implicitWidth + implicitHeight: container.implicitHeight - General.rectMargin - IRect { - id: container - anchors { - fill: parent - verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter - } + IRect { + id: container + anchors { + fill: parent + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } - implicitWidth: layout.implicitWidth + General.rectMargin * 2 - implicitHeight: Bar.height - color: Colors.accent - radius: Style.rounding.large + implicitWidth: layout.implicitWidth + General.rectMargin * 2 + implicitHeight: Bar.height + color: Colors.accent + radius: Style.rounding.large - RowLayout { - id: layout - anchors { - verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter - } - spacing: Bar.resourceIconTextSpacing + RowLayout { + id: layout + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + spacing: Bar.resourceIconTextSpacing - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - Icon { - visible: root.isCollapsed - text: Services.Network.state - } - IText { - visible: !root.isCollapsed - text: Services.Network.networkStrength - renderType: Text.CurveRendering // it's not static and is rapidly updated - } + Icon { + visible: root.isCollapsed + text: Services.Network.state + } + IText { + visible: !root.isCollapsed + text: Services.Network.networkStrength + renderType: Text.CurveRendering // it's not static and is rapidly updated + } - ColumnLayout { - // spacing: Bar.resourceIconTextSpacing / 2 - spacing: 0 - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - IText { - animate: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - text: root.down + root.downUnit - color: root.downUnit == root.usageUnit && root.down > root.significantUsage ? Colors.cyan : Colors.foreground2 - renderType: Text.CurveRendering // it's not static and is rapidly updated - } - Icon { - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - text: Icons.resource.network.download - iconSize: Style.font.size.normal - color: root.downUnit == root.usageUnit && root.down > root.significantUsage ? Colors.cyan : Colors.foreground2 - } - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - } - ColumnLayout { - // spacing: Bar.resourceIconTextSpacing / 2 - spacing: 0 - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - Icon { - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - text: Icons.resource.network.upload - iconSize: Style.font.size.normal - color: root.upUnit == root.usageUnit && root.up > root.significantUsage ? Colors.cyan : Colors.foreground2 - } - IText { - animate: true - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - text: root.up + root.upUnit - color: root.upUnit == root.usageUnit && root.up > root.significantUsage ? Colors.cyan : Colors.foreground2 - renderType: Text.CurveRendering // it's not static and is rapidly updated - } - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - } + ColumnLayout { + // spacing: Bar.resourceIconTextSpacing / 2 + spacing: 0 + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + IText { + animate: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + text: root.down + root.downUnit + color: root.downUnit == root.usageUnit && root.down > root.significantUsage ? Colors.cyan : Colors.foreground2 + renderType: Text.CurveRendering // it's not static and is rapidly updated + } + Icon { + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + text: Icons.resource.network.download + iconSize: Style.font.size.normal + color: root.downUnit == root.usageUnit && root.down > root.significantUsage ? Colors.cyan : Colors.foreground2 + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } + ColumnLayout { + // spacing: Bar.resourceIconTextSpacing / 2 + spacing: 0 + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + Icon { + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + text: Icons.resource.network.upload + iconSize: Style.font.size.normal + color: root.upUnit == root.usageUnit && root.up > root.significantUsage ? Colors.cyan : Colors.foreground2 + } + IText { + animate: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + text: root.up + root.upUnit + color: root.upUnit == root.usageUnit && root.up > root.significantUsage ? Colors.cyan : Colors.foreground2 + renderType: Text.CurveRendering // it's not static and is rapidly updated + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } - Item { - Layout.fillWidth: true - } - } - } - MouseArea { - anchors.fill: container - cursorShape: Qt.PointingHandCursor - onClicked: { - root.isCollapsed = !root.isCollapsed; - } - } + Item { + Layout.fillWidth: true + } + } + } + MouseArea { + anchors.fill: container + cursorShape: Qt.PointingHandCursor + onClicked: { + root.isCollapsed = !root.isCollapsed; + } + } } diff --git a/bar/Power.qml b/bar/Power.qml index a29bc4a..289f8e8 100644 --- a/bar/Power.qml +++ b/bar/Power.qml @@ -8,117 +8,117 @@ import qs.components // TODO: move to a full screen menu Item { - id: root - - implicitWidth: container.implicitWidth - implicitHeight: container.implicitHeight - - property bool collapsed: true - - IRect { - id: container - - anchors { - fill: parent - verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter - } - implicitWidth: layout.implicitWidth + General.rectMargin * 2 - implicitHeight: Bar.height - General.rectMargin - - color: Colors.accent - radius: Style.rounding.large - - RowLayout { - id: layout - - anchors.fill: parent - spacing: Bar.resourceIconTextSpacing - - Item { - Layout.fillWidth: true - } - - Icon { - id: powerIcon - text: Icons.power.shutdown - font.pixelSize: Style.font.size.large - color: Colors.red - } - - Item { - implicitWidth: root.collapsed ? 0 : loader.width - visible: root.collapsed ? false : true - Loader { - id: loader - anchors.verticalCenter: parent.verticalCenter - active: !root.collapsed - sourceComponent: Component { - RowLayout { - spacing: Bar.resourceIconTextSpacing - Icon { - text: Icons.power.dpms - font.pixelSize: Style.font.size.large - } - - Icon { - text: Icons.power.lock - font.pixelSize: Style.font.size.large - } - - Icon { - text: Icons.power.suspend - font.pixelSize: Style.font.size.large - } - - Icon { - text: Icons.power.reboot - font.pixelSize: Style.font.size.large - } - SequentialAnimation { - running: true - NumberAnimation { - target: parent - property: "opacity" - from: 0 - to: 1 - duration: General.animationDuration / 2 - easing.type: Easing.InOutQuad - } - } - } - } - } - Behavior on implicitWidth { - NumberAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - - // no idea why when collapsing the width animation won't trigger - Behavior on visible { - NumberAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - } - - Item { - Layout.fillWidth: true - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - root.collapsed = false; - } - onExited: { - root.collapsed = true; - } - } - } + id: root + + implicitWidth: container.implicitWidth + implicitHeight: container.implicitHeight + + property bool collapsed: true + + IRect { + id: container + + anchors { + fill: parent + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + implicitWidth: layout.implicitWidth + General.rectMargin * 2 + implicitHeight: Bar.height - General.rectMargin + + color: Colors.accent + radius: Style.rounding.large + + RowLayout { + id: layout + + anchors.fill: parent + spacing: Bar.resourceIconTextSpacing + + Item { + Layout.fillWidth: true + } + + Icon { + id: powerIcon + text: Icons.power.shutdown + font.pixelSize: Style.font.size.large + color: Colors.red + } + + Item { + implicitWidth: root.collapsed ? 0 : loader.width + visible: root.collapsed ? false : true + Loader { + id: loader + anchors.verticalCenter: parent.verticalCenter + active: !root.collapsed + sourceComponent: Component { + RowLayout { + spacing: Bar.resourceIconTextSpacing + Icon { + text: Icons.power.dpms + font.pixelSize: Style.font.size.large + } + + Icon { + text: Icons.power.lock + font.pixelSize: Style.font.size.large + } + + Icon { + text: Icons.power.suspend + font.pixelSize: Style.font.size.large + } + + Icon { + text: Icons.power.reboot + font.pixelSize: Style.font.size.large + } + SequentialAnimation { + running: true + NumberAnimation { + target: parent + property: "opacity" + from: 0 + to: 1 + duration: General.animationDuration / 2 + easing.type: Easing.InOutQuad + } + } + } + } + } + Behavior on implicitWidth { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + + // no idea why when collapsing the width animation won't trigger + Behavior on visible { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + } + + Item { + Layout.fillWidth: true + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + root.collapsed = false; + } + onExited: { + root.collapsed = true; + } + } + } } diff --git a/bar/Resources.qml b/bar/Resources.qml index a4d2a2f..943bd52 100644 --- a/bar/Resources.qml +++ b/bar/Resources.qml @@ -7,53 +7,53 @@ import qs.config as Config import qs.services as Services Item { - implicitWidth: container.implicitWidth - implicitHeight: container.implicitHeight + implicitWidth: container.implicitWidth + implicitHeight: container.implicitHeight - IRect { - id: container - implicitWidth: layout.implicitWidth + Config.General.rectMargin * 2 - implicitHeight: Config.Bar.height - Config.General.rectMargin + IRect { + id: container + implicitWidth: layout.implicitWidth + Config.General.rectMargin * 2 + implicitHeight: Config.Bar.height - Config.General.rectMargin - color: Config.Colors.accent - radius: Config.Style.rounding.large + color: Config.Colors.accent + radius: Config.Style.rounding.large - Row { - id: layout - anchors.centerIn: parent - SimpleResource { - value: Math.round(Services.Resource.cpuUsage * 100) - altValue: Math.round(Services.Resource.cpuFreq / 10) / 100 - suffix: "%" - altSuffix: "GHz" - icon: Config.Icons.resource.cpu - } - SimpleResource { - icon: Config.Icons.resource.memory - value: Math.round(Services.Resource.memoryUsedPercentage * 100) - altValue: Math.round(Services.Resource.memoryUsed / 1024 / 1024) - suffix: "%" - altSuffix: "GB" - } - SimpleResource { - icon: Config.Icons.resource.gpu - value: Services.Resource.gpuUsage - altValue: Services.Resource.gpuTemp - suffix: "%" - altSuffix: "°C" - altIcon: Config.Icons.resource.temp - thresholdL1: 60 - thresholdL2: 70 - thresholdL3: 80 - } - SimpleResource { - icon: Config.Icons.resource.temp - value: Math.round(Services.Resource.cpuTemp * 100) - suffix: "°C" - thresholdL1: 60 - thresholdL2: 80 - thresholdL3: 90 - } - } - } + Row { + id: layout + anchors.centerIn: parent + SimpleResource { + value: Math.round(Services.Resource.cpuUsage * 100) + altValue: Math.round(Services.Resource.cpuFreq / 10) / 100 + suffix: "%" + altSuffix: "GHz" + icon: Config.Icons.resource.cpu + } + SimpleResource { + icon: Config.Icons.resource.memory + value: Math.round(Services.Resource.memoryUsedPercentage * 100) + altValue: Math.round(Services.Resource.memoryUsed / 1024 / 1024) + suffix: "%" + altSuffix: "GB" + } + SimpleResource { + icon: Config.Icons.resource.gpu + value: Services.Resource.gpuUsage + altValue: Services.Resource.gpuTemp + suffix: "%" + altSuffix: "°C" + altIcon: Config.Icons.resource.temp + thresholdL1: 60 + thresholdL2: 70 + thresholdL3: 80 + } + SimpleResource { + icon: Config.Icons.resource.temp + value: Math.round(Services.Resource.cpuTemp * 100) + suffix: "°C" + thresholdL1: 60 + thresholdL2: 80 + thresholdL3: 90 + } + } + } } diff --git a/bar/TimeDate.qml b/bar/TimeDate.qml index 2716e6d..ddeca47 100644 --- a/bar/TimeDate.qml +++ b/bar/TimeDate.qml @@ -6,128 +6,128 @@ import qs.components import qs.config Item { - id: root + id: root - property bool isAlt: false - property bool isCollapsed: false + property bool isAlt: false + property bool isCollapsed: false - implicitWidth: container.implicitWidth - implicitHeight: container.implicitHeight - General.rectMargin - IRect { - id: container - anchors { - fill: parent - centerIn: parent - } + implicitWidth: container.implicitWidth + implicitHeight: container.implicitHeight - General.rectMargin + IRect { + id: container + anchors { + fill: parent + centerIn: parent + } - // implicitWidth: !isAlt ? nonAlt.implicitWidth + General.rectMargin * 2 : alt.implicitWidth + General.rectMargin * 2 - implicitWidth: layout.width + General.rectMargin * 2 - implicitHeight: Bar.height + // implicitWidth: !isAlt ? nonAlt.implicitWidth + General.rectMargin * 2 : alt.implicitWidth + General.rectMargin * 2 + implicitWidth: layout.width + General.rectMargin * 2 + implicitHeight: Bar.height - color: Colors.accent - radius: Style.rounding.large + color: Colors.accent + radius: Style.rounding.large - RowLayout { - id: layout - anchors.centerIn: parent - spacing: Bar.resourceIconTextSpacing - Loader { - sourceComponent: !root.isAlt ? main : alt - readonly property Component main: Component { - RowLayout { - id: root - property int h: Services.TimeDate.hours - property int m: Services.TimeDate.minutes - property int s: Services.TimeDate.seconds + RowLayout { + id: layout + anchors.centerIn: parent + spacing: Bar.resourceIconTextSpacing + Loader { + sourceComponent: !root.isAlt ? main : alt + readonly property Component main: Component { + RowLayout { + id: root + property int h: Services.TimeDate.hours + property int m: Services.TimeDate.minutes + property int s: Services.TimeDate.seconds - Item { - Layout.fillWidth: true - } - anchors.centerIn: parent - spacing: Bar.resourceIconTextSpacing + Item { + Layout.fillWidth: true + } + anchors.centerIn: parent + spacing: Bar.resourceIconTextSpacing - Icon { - text: Icons.resource.clock[root.h - 1] - font.pixelSize: Style.font.size.larger - } - RowLayout { - spacing: 0 - IText { - animate: true - text: (root.h >= 10 ? "" : "0") + root.h - } - IText { - text: ":" - renderType: Text.CurveRendering - } - IText { - animate: true - text: (root.m >= 10 ? "" : "0") + root.m - } - IText { - text: ":" - renderType: Text.CurveRendering - } - IText { - animate: true - text: (root.s >= 10 ? "" : "0") + root.s - renderType: Text.QtRendering // it's not static and is rapidly updated - } - } - Item { - Layout.fillWidth: true - } - SequentialAnimation { - running: true - NumberAnimation { - target: parent - property: "opacity" - from: 0 - to: 1 - duration: General.animationDuration / 2 - } - } - } - } - readonly property Component alt: Component { - RowLayout { - Item { - Layout.fillWidth: true - } - anchors.centerIn: parent - spacing: Bar.resourceIconTextSpacing + Icon { + text: Icons.resource.clock[root.h - 1] + font.pixelSize: Style.font.size.larger + } + RowLayout { + spacing: 0 + IText { + animate: true + text: (root.h >= 10 ? "" : "0") + root.h + } + IText { + text: ":" + renderType: Text.CurveRendering + } + IText { + animate: true + text: (root.m >= 10 ? "" : "0") + root.m + } + IText { + text: ":" + renderType: Text.CurveRendering + } + IText { + animate: true + text: (root.s >= 10 ? "" : "0") + root.s + renderType: Text.QtRendering // it's not static and is rapidly updated + } + } + Item { + Layout.fillWidth: true + } + SequentialAnimation { + running: true + NumberAnimation { + target: parent + property: "opacity" + from: 0 + to: 1 + duration: General.animationDuration / 2 + } + } + } + } + readonly property Component alt: Component { + RowLayout { + Item { + Layout.fillWidth: true + } + anchors.centerIn: parent + spacing: Bar.resourceIconTextSpacing - Icon { - text: Icons.resource.calendar - font.pixelSize: Style.font.size.larger - } - IText { - animate: true - text: Services.TimeDate.date - } - Item { - Layout.fillWidth: true - } - SequentialAnimation { - running: true - NumberAnimation { - target: parent - property: "opacity" - from: 0 - to: 1 - duration: General.animationDuration / 2 - } - } - } - } - } - } - } - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onPressed: { - root.isAlt = !root.isAlt; - } - } + Icon { + text: Icons.resource.calendar + font.pixelSize: Style.font.size.larger + } + IText { + animate: true + text: Services.TimeDate.date + } + Item { + Layout.fillWidth: true + } + SequentialAnimation { + running: true + NumberAnimation { + target: parent + property: "opacity" + from: 0 + to: 1 + duration: General.animationDuration / 2 + } + } + } + } + } + } + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onPressed: { + root.isAlt = !root.isAlt; + } + } } diff --git a/bar/Workspace.qml b/bar/Workspace.qml index 52f2d1b..51c4ba4 100644 --- a/bar/Workspace.qml +++ b/bar/Workspace.qml @@ -6,241 +6,241 @@ import qs.config import qs.components Item { - id: root - - implicitWidth: layout.implicitWidth - - property var ws: Hyprland.workspaces - property var toplevels: Hyprland.toplevels.values // update activeOccupied when toplevels changes as getWorkspaceStat's return is non reactive - - property int activeIndex: -1 - property bool activeOccupied: false - property bool toChild: false - - function getWorkspaceStats(index) { - const w = ws.values.find(i => i.id === index + 1); - return { - isOccupied: w?.toplevels?.values?.length, - isActive: w?.active, - isUrgent: w?.urgent - }; - } - - onToplevelsChanged: { - Qt.callLater(() => { - activeOccupied = getWorkspaceStats(activeIndex).isOccupied || false; - }); - } - - DropShadow { - anchors.fill: activeRect - source: activeRect - radius: 8 - cached: true - color: root.activeOccupied ? Colors.accent : Colors.foreground1 - opacity: root.activeOccupied ? Bar.wsActiveOpacity : Bar.wsEmptyOpacity / 3 - - Behavior on color { - ColorAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - Behavior on opacity { - NumberAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - } - IRect { - id: activeRect - x: layout.x + root.activeIndex * Bar.wsIconSize + root.activeIndex * Bar.wsSpacing - anchors.verticalCenter: layout.verticalCenter - implicitWidth: Bar.wsActiveIconSize - implicitHeight: Bar.wsIconSize - Bar.wsHorizontalSpacing - radius: Bar.wsRounding - color: root.activeOccupied ? Colors.accent : Colors.foreground1 - opacity: root.activeOccupied ? Bar.wsActiveOpacity : Bar.wsEmptyOpacity / 3 - Behavior on x { - ISpringAnimation {} - } - Behavior on color { - ColorAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - Behavior on opacity { - NumberAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - Behavior on width { - NumberAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - } - - RowLayout { - id: layout - anchors.verticalCenter: parent.verticalCenter - implicitWidth: repeater.width - spacing: Bar.wsSpacing - - Repeater { - id: repeater - model: Bar.wss - - IRect { - property var st: getWorkspaceStats(index) - property bool active: st?.isActive || false - - implicitWidth: active ? Bar.wsActiveIconSize : Bar.wsIconSize - implicitHeight: Bar.wsIconSize - Bar.wsHorizontalSpacing - radius: Bar.wsRounding - color: st.isOccupied ? Colors.accent : Colors.foreground1 - opacity: active && st.isOccupied ? Bar.wsActiveOpacity : st.isOccupied ? Bar.wsOpacity : Bar.wsEmptyOpacity - - Icon { - anchors.centerIn: parent - text: !st.isUrgent ? Icons.ws[index + 1] : Icons.ws['urgent'] - font.pixelSize: !active ? Bar.wsIconSize / 2 : Bar.wsActiveIconSize / 2.5 - opacity: !st.isOccupied ? 0 : 1 - color: !st.isUrgent ? !st.isActive ? Colors.foreground2 : Colors.foreground1 : Colors.red - Behavior on font.pixelSize { - ISpringAnimation {} - } - Behavior on color { - ColorAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - } - - MouseArea { - anchors { - fill: parent - topMargin: Bar.wsExtraMouseArea * -1 - bottomMargin: Bar.wsExtraMouseArea * -1 - } - - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - hoverEnabled: true - - onPressed: { - if (!parent.active) - Hyprland.dispatch(`hl.dsp.focus({ workspace = '${index + 1}' })`); - } - function moveActive() { - activeRect.x = parent.x; - root.activeOccupied = st.isOccupied || false; - activeRect.implicitWidth = active ? Bar.wsActiveIconSize : Bar.wsIconSize; - } - onEntered: { - root.toChild = true; - moveActive(); - } - // https://github.com/quickshell-mirror/quickshell/issues/118 - // onEntered: { - // moveActive(); - // } - onExited: { - root.toChild = false; - // outer MouseArea will handle this - // if (!active && previousActiveIndex >= 0) { - // // just in case - // activeRect.x = layout.x + previousActiveIndex * Bar.wsIconSize + previousActiveIndex * Bar.wsSpacing; - // activeRect.implicitWidth = Bar.wsActiveIconSize; - // // root.activeOccupied = parent.st.isOccupied || false; - // root.activeOccupied = getWorkspaceStats(previousActiveIndex).isOccupied || false; - // previousActiveIndex = -1; - // } - } - onWheel: event => { - // no idea wats wrong w/ prev so we just unify it to use +/- 1 - if (event.angleDelta.y < 0) { - if (activeIndex + 1 < Bar.wss) - Hyprland.dispatch(`hl.dsp.focus({ workspace = '+1' })`); - else - Hyprland.dispatch(`hl.dsp.focus({ workspace = '1' })`); - } else if (event.angleDelta.y > 0) { - if (activeIndex + 1 > 1) { - Hyprland.dispatch(`hl.dsp.focus({ workspace = '-1' })`); - } else if (Bar.wss > 1) { - Hyprland.dispatch(`hl.dsp.focus({ workspace = '${Bar.wss}' })`); - } - } - } - } - - Behavior on opacity { - NumberAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - Behavior on implicitWidth { - NumberAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - Behavior on color { - ColorAnimation { - duration: Bar.wsAnimationDuration - easing.type: Easing.InOutQuad - } - } - - onActiveChanged: if (active) { - root.activeIndex = index; - root.activeOccupied = st.isOccupied; - activeRect.implicitWidth = Bar.wsActiveIconSize; - } - } - } - } - MouseArea { - height: Bar.height - width: parent.width - hoverEnabled: true - z: -1000 // to be below the child's MouseArea - acceptedButtons: Qt.MiddleButton - - // sync w the inner MouseArea - // https://github.com/quickshell-mirror/quickshell/issues/118 // but onEnter won't update the mouseX - onEntered: { - const abovedItemIndex = Math.round((mouseX - layout.x) / (Bar.wsIconSize + Bar.wsSpacing)) - 1; // TODO: workspaceActiveIconSize might be before - activeRect.x = mouseX - activeRect.width / 2; - root.activeOccupied = root.getWorkspaceStats(abovedItemIndex).isOccupied || false; - activeRect.implicitWidth = root.activeIndex === abovedItemIndex ? Bar.wsActiveIconSize : Bar.wsIconSize; - } - onExited: { - if (!root.toChild && root.activeIndex >= 0) { - // just in case - activeRect.x = layout.x + root.activeIndex * Bar.wsIconSize + root.activeIndex * Bar.wsSpacing; - activeRect.implicitWidth = Bar.wsActiveIconSize; - root.activeOccupied = root.getWorkspaceStats(root.activeIndex).isOccupied || false; - } - } - onWheel: event => { - // no idea wats wrong w/ prev so we just unify it to use +/- 1 - if (event.angleDelta.y < 0) { - if (root.activeIndex + 1 < Bar.wss) - Hyprland.dispatch(`hl.dsp.focus({ workspace = '+1' })`); - else - Hyprland.dispatch(`hl.dsp.focus({ workspace = '1' })`); - } else if (event.angleDelta.y > 0) { - if (root.activeIndex + 1 > 1) - Hyprland.dispatch(`hl.dsp.focus({ workspace = '1' })`); - else if (Bar.wss > 1) - Hyprland.dispatch(`hl.dsp.focus({ workspace = '${Bar.wss}' })`); - } - } - } + id: root + + implicitWidth: layout.implicitWidth + + property var ws: Hyprland.workspaces + property var toplevels: Hyprland.toplevels.values // update activeOccupied when toplevels changes as getWorkspaceStat's return is non reactive + + property int activeIndex: -1 + property bool activeOccupied: false + property bool toChild: false + + function getWorkspaceStats(index) { + const w = ws.values.find(i => i.id === index + 1); + return { + isOccupied: w?.toplevels?.values?.length, + isActive: w?.active, + isUrgent: w?.urgent + }; + } + + onToplevelsChanged: { + Qt.callLater(() => { + activeOccupied = getWorkspaceStats(activeIndex).isOccupied || false; + }); + } + + DropShadow { + anchors.fill: activeRect + source: activeRect + radius: 8 + cached: true + color: root.activeOccupied ? Colors.accent : Colors.foreground1 + opacity: root.activeOccupied ? Bar.wsActiveOpacity : Bar.wsEmptyOpacity / 3 + + Behavior on color { + ColorAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + Behavior on opacity { + NumberAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + } + IRect { + id: activeRect + x: layout.x + root.activeIndex * Bar.wsIconSize + root.activeIndex * Bar.wsSpacing + anchors.verticalCenter: layout.verticalCenter + implicitWidth: Bar.wsActiveIconSize + implicitHeight: Bar.wsIconSize - Bar.wsHorizontalSpacing + radius: Bar.wsRounding + color: root.activeOccupied ? Colors.accent : Colors.foreground1 + opacity: root.activeOccupied ? Bar.wsActiveOpacity : Bar.wsEmptyOpacity / 3 + Behavior on x { + ISpringAnimation {} + } + Behavior on color { + ColorAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + Behavior on opacity { + NumberAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + Behavior on width { + NumberAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + } + + RowLayout { + id: layout + anchors.verticalCenter: parent.verticalCenter + implicitWidth: repeater.width + spacing: Bar.wsSpacing + + Repeater { + id: repeater + model: Bar.wss + + IRect { + property var st: getWorkspaceStats(index) + property bool active: st?.isActive || false + + implicitWidth: active ? Bar.wsActiveIconSize : Bar.wsIconSize + implicitHeight: Bar.wsIconSize - Bar.wsHorizontalSpacing + radius: Bar.wsRounding + color: st.isOccupied ? Colors.accent : Colors.foreground1 + opacity: active && st.isOccupied ? Bar.wsActiveOpacity : st.isOccupied ? Bar.wsOpacity : Bar.wsEmptyOpacity + + Icon { + anchors.centerIn: parent + text: !st.isUrgent ? Icons.ws[index + 1] : Icons.ws['urgent'] + font.pixelSize: !active ? Bar.wsIconSize / 2 : Bar.wsActiveIconSize / 2.5 + opacity: !st.isOccupied ? 0 : 1 + color: !st.isUrgent ? !st.isActive ? Colors.foreground2 : Colors.foreground1 : Colors.red + Behavior on font.pixelSize { + ISpringAnimation {} + } + Behavior on color { + ColorAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + } + + MouseArea { + anchors { + fill: parent + topMargin: Bar.wsExtraMouseArea * -1 + bottomMargin: Bar.wsExtraMouseArea * -1 + } + + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + + onPressed: { + if (!parent.active) + Hyprland.dispatch(`hl.dsp.focus({ workspace = '${index + 1}' })`); + } + function moveActive() { + activeRect.x = parent.x; + root.activeOccupied = st.isOccupied || false; + activeRect.implicitWidth = active ? Bar.wsActiveIconSize : Bar.wsIconSize; + } + onEntered: { + root.toChild = true; + moveActive(); + } + // https://github.com/quickshell-mirror/quickshell/issues/118 + // onEntered: { + // moveActive(); + // } + onExited: { + root.toChild = false; + // outer MouseArea will handle this + // if (!active && previousActiveIndex >= 0) { + // // just in case + // activeRect.x = layout.x + previousActiveIndex * Bar.wsIconSize + previousActiveIndex * Bar.wsSpacing; + // activeRect.implicitWidth = Bar.wsActiveIconSize; + // // root.activeOccupied = parent.st.isOccupied || false; + // root.activeOccupied = getWorkspaceStats(previousActiveIndex).isOccupied || false; + // previousActiveIndex = -1; + // } + } + onWheel: event => { + // no idea wats wrong w/ prev so we just unify it to use +/- 1 + if (event.angleDelta.y < 0) { + if (activeIndex + 1 < Bar.wss) + Hyprland.dispatch(`hl.dsp.focus({ workspace = '+1' })`); + else + Hyprland.dispatch(`hl.dsp.focus({ workspace = '1' })`); + } else if (event.angleDelta.y > 0) { + if (activeIndex + 1 > 1) { + Hyprland.dispatch(`hl.dsp.focus({ workspace = '-1' })`); + } else if (Bar.wss > 1) { + Hyprland.dispatch(`hl.dsp.focus({ workspace = '${Bar.wss}' })`); + } + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + Behavior on implicitWidth { + NumberAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + Behavior on color { + ColorAnimation { + duration: Bar.wsAnimationDuration + easing.type: Easing.InOutQuad + } + } + + onActiveChanged: if (active) { + root.activeIndex = index; + root.activeOccupied = st.isOccupied; + activeRect.implicitWidth = Bar.wsActiveIconSize; + } + } + } + } + MouseArea { + height: Bar.height + width: parent.width + hoverEnabled: true + z: -1000 // to be below the child's MouseArea + acceptedButtons: Qt.MiddleButton + + // sync w the inner MouseArea + // https://github.com/quickshell-mirror/quickshell/issues/118 // but onEnter won't update the mouseX + onEntered: { + const abovedItemIndex = Math.round((mouseX - layout.x) / (Bar.wsIconSize + Bar.wsSpacing)) - 1; // TODO: workspaceActiveIconSize might be before + activeRect.x = mouseX - activeRect.width / 2; + root.activeOccupied = root.getWorkspaceStats(abovedItemIndex).isOccupied || false; + activeRect.implicitWidth = root.activeIndex === abovedItemIndex ? Bar.wsActiveIconSize : Bar.wsIconSize; + } + onExited: { + if (!root.toChild && root.activeIndex >= 0) { + // just in case + activeRect.x = layout.x + root.activeIndex * Bar.wsIconSize + root.activeIndex * Bar.wsSpacing; + activeRect.implicitWidth = Bar.wsActiveIconSize; + root.activeOccupied = root.getWorkspaceStats(root.activeIndex).isOccupied || false; + } + } + onWheel: event => { + // no idea wats wrong w/ prev so we just unify it to use +/- 1 + if (event.angleDelta.y < 0) { + if (root.activeIndex + 1 < Bar.wss) + Hyprland.dispatch(`hl.dsp.focus({ workspace = '+1' })`); + else + Hyprland.dispatch(`hl.dsp.focus({ workspace = '1' })`); + } else if (event.angleDelta.y > 0) { + if (root.activeIndex + 1 > 1) + Hyprland.dispatch(`hl.dsp.focus({ workspace = '1' })`); + else if (Bar.wss > 1) + Hyprland.dispatch(`hl.dsp.focus({ workspace = '${Bar.wss}' })`); + } + } + } } diff --git a/bar/components/SimpleResource.qml b/bar/components/SimpleResource.qml index 5e873f9..cb2fef6 100644 --- a/bar/components/SimpleResource.qml +++ b/bar/components/SimpleResource.qml @@ -6,95 +6,95 @@ import qs.components import QtQuick.Controls Item { - id: root + id: root - // TODO: use https://quickshell.org/docs/master/types/Quickshell/PopupWindow/ for tooltip + // TODO: use https://quickshell.org/docs/master/types/Quickshell/PopupWindow/ for tooltip - required property real value - required property string suffix // unit - required property string icon - property string altValue: value - property string altSuffix: suffix - property string altIcon: icon - property string text: root.value + root.suffix - property string altText: root.altValue + root.altSuffix - property real thresholdL1: 40 - property real thresholdL2: thresholdL1 * 1.5 - property real thresholdL3: thresholdL2 * 1.5 - property color thresholdColorL1: Colors.cyan - property color thresholdColorL2: Colors.yellow - property color thresholdColorL3: Colors.red + required property real value + required property string suffix // unit + required property string icon + property string altValue: value + property string altSuffix: suffix + property string altIcon: icon + property string text: root.value + root.suffix + property string altText: root.altValue + root.altSuffix + property real thresholdL1: 40 + property real thresholdL2: thresholdL1 * 1.5 + property real thresholdL3: thresholdL2 * 1.5 + property color thresholdColorL1: Colors.cyan + property color thresholdColorL2: Colors.yellow + property color thresholdColorL3: Colors.red - property bool isAlt: false + property bool isAlt: false - property color color: value < thresholdL1 ? Colors.accentAlt : value >= thresholdL3 ? thresholdColorL3 : value >= thresholdL2 ? thresholdColorL2 : thresholdColorL1 + property color color: value < thresholdL1 ? Colors.accentAlt : value >= thresholdL3 ? thresholdColorL3 : value >= thresholdL2 ? thresholdColorL2 : thresholdColorL1 - implicitWidth: layout.implicitWidth - implicitHeight: layout.implicitHeight + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight - RowLayout { - id: layout + RowLayout { + id: layout - spacing: 0 + spacing: 0 - Item { - Layout.fillWidth: true - } + Item { + Layout.fillWidth: true + } - IRect { - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: m_icon.width + General.rectMargin * 2 - implicitHeight: Bar.height - General.rectMargin - color: "transparent" - Slider { - id: pBar - anchors.fill: parent - handle: Item - from: 0 - to: 100 - value: !root.isAlt ? root.value : root.altValue - background: IRect { - anchors.fill: parent - color: "transparent" - // radius: Style.rounding.smaller - } - contentItem: Item { - IRect { - anchors.bottom: parent.bottom - width: parent.width - height: parent.height * pBar.visualPosition - color: Qt.rgba(root.color.r, root.color.g, root.color.b, General.accentTransparency * 1.5) - radius: Style.rounding.smaller + IRect { + Layout.fillWidth: true + Layout.fillHeight: true + implicitWidth: m_icon.width + General.rectMargin * 2 + implicitHeight: Bar.height - General.rectMargin + color: "transparent" + Slider { + id: pBar + anchors.fill: parent + handle: Item + from: 0 + to: 100 + value: !root.isAlt ? root.value : root.altValue + background: IRect { + anchors.fill: parent + color: "transparent" + // radius: Style.rounding.smaller + } + contentItem: Item { + IRect { + anchors.bottom: parent.bottom + width: parent.width + height: parent.height * pBar.visualPosition + color: Qt.rgba(root.color.r, root.color.g, root.color.b, General.accentTransparency * 1.5) + radius: Style.rounding.smaller - Behavior on height { - NumberAnimation { - duration: General.animationDuration / 2 - } - } - } - } - } - Icon { - id: m_icon - anchors { - centerIn: parent - } - iconSize: Style.font.size.small - text: !root.isAlt ? root.icon : root.altIcon - color: root.value < root.thresholdL1 ? Colors.foreground2 : root.value >= root.thresholdL3 ? root.thresholdColorL3 : root.value >= root.thresholdL2 ? root.thresholdColorL2 : root.thresholdColorL1 - } - } + Behavior on height { + NumberAnimation { + duration: General.animationDuration / 2 + } + } + } + } + } + Icon { + id: m_icon + anchors { + centerIn: parent + } + iconSize: Style.font.size.small + text: !root.isAlt ? root.icon : root.altIcon + color: root.value < root.thresholdL1 ? Colors.foreground2 : root.value >= root.thresholdL3 ? root.thresholdColorL3 : root.value >= root.thresholdL2 ? root.thresholdColorL2 : root.thresholdColorL1 + } + } - Item { - Layout.fillWidth: true - } - } - MouseArea { - anchors.fill: parent - cursorShape: altValue != value ? Qt.PointingHandCursor : Qt.ArrowCursor - onPressed: { - root.isAlt = !root.isAlt; - } - } + Item { + Layout.fillWidth: true + } + } + MouseArea { + anchors.fill: parent + cursorShape: altValue != value ? Qt.PointingHandCursor : Qt.ArrowCursor + onPressed: { + root.isAlt = !root.isAlt; + } + } } diff --git a/components/IListView.qml b/components/IListView.qml new file mode 100644 index 0000000..d9155f1 --- /dev/null +++ b/components/IListView.qml @@ -0,0 +1,34 @@ +import QtQuick + +ListView { + id: root + + property PropertyAnimation scrollAnim: ISpringAnimation {} + + function scrollToIndex(idx, alignment = ListView.Contain) { + root.scrollAnim.stop(); + let y = root.contentY; + + root.positionViewAtIndex(idx, alignment); + let dsty = root.contentY; + + root.contentY = y; + + if (root.scrollAnim) { + root.scrollAnim.target = root; + root.scrollAnim.property = "contentY"; + root.scrollAnim.from = y; + root.scrollAnim.to = dsty; + root.scrollAnim.start(); + } else + root.contentY = dsty; + } + + Connections { + target: root.scrollAnim + + function onStopped() { + // root.positionViewAtIndex(targetIndex, alignment); + } + } +} diff --git a/components/IMouseArea.qml b/components/IMouseArea.qml index 4c360ed..e817bca 100644 --- a/components/IMouseArea.qml +++ b/components/IMouseArea.qml @@ -4,42 +4,42 @@ import QtQuick MouseArea { - hoverEnabled: true + hoverEnabled: true - property real gx: Number.MAX_VALUE - property real gy: Number.MAX_VALUE + property real gx: Number.MAX_VALUE + property real gy: Number.MAX_VALUE - property bool hovered: false + property bool hovered: false - signal entered_ - signal positionChanged_(var mouse) + signal entered_ + signal positionChanged_(var mouse) - onPositionChanged: mouse => { - let gpos = mapToItem(null, mouse.x, mouse.y); + onPositionChanged: mouse => { + let gpos = mapToItem(null, mouse.x, mouse.y); - if (gx === Number.MAX_VALUE && gy === Number.MAX_VALUE) { - gx = gpos.x; - gy = gpos.y; - return; - } + if (gx === Number.MAX_VALUE && gy === Number.MAX_VALUE) { + gx = gpos.x; + gy = gpos.y; + return; + } - if (gpos.x === gx && gpos.y === gy) - return; + if (gpos.x === gx && gpos.y === gy) + return; - gx = gpos.x; - gy = gpos.y; + gx = gpos.x; + gy = gpos.y; - if (!hovered) { - hovered = true; - entered_(); - } + if (!hovered) { + hovered = true; + entered_(); + } - positionChanged_(mouse); - } + positionChanged_(mouse); + } - onExited: { - hovered = false; - gx = Number.MAX_VALUE; - gy = Number.MAX_VALUE; - } + onExited: { + hovered = false; + gx = Number.MAX_VALUE; + gy = Number.MAX_VALUE; + } } diff --git a/components/IRect.qml b/components/IRect.qml index a72b564..5f9a86f 100644 --- a/components/IRect.qml +++ b/components/IRect.qml @@ -2,18 +2,18 @@ import QtQuick import qs.config Rectangle { - Behavior on color { - ColorAnimation { - duration: General.animationDuration - easing.type: Easing.InOutQuad - } - } + Behavior on color { + ColorAnimation { + duration: General.animationDuration + easing.type: Easing.InOutQuad + } + } - // TODO: use visible instead of opacity for perf, but we need to animate the opacity then set visible - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration - easing.type: Easing.InOutQuad - } - } + // TODO: use visible instead of opacity for perf, but we need to animate the opacity then set visible + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration + easing.type: Easing.InOutQuad + } + } } diff --git a/components/ISpringAnimation.qml b/components/ISpringAnimation.qml index 0d13b59..cac1138 100644 --- a/components/ISpringAnimation.qml +++ b/components/ISpringAnimation.qml @@ -2,8 +2,8 @@ import QtQuick import qs.config SpringAnimation { - property real speed: 1.0 + property real speed: 1.0 - spring: General.springAnimationSpring * (speed * speed) - damping: General.springAnimationDamping * (speed) + spring: General.springAnimationSpring * (speed * speed) + damping: General.springAnimationDamping * (speed) } diff --git a/components/IText.qml b/components/IText.qml index c3eb817..54878c5 100644 --- a/components/IText.qml +++ b/components/IText.qml @@ -5,66 +5,66 @@ import QtQuick import qs.config Text { - id: root + id: root - property bool animate: false - property real animateFrom: 0 - property real animateTo: 1 - property real multiplier: 1.0 - property real fontSize: Style.font.size.larger + property bool animate: false + property real animateFrom: 0 + property real animateTo: 1 + property real multiplier: 1.0 + property real fontSize: Style.font.size.larger - renderType: Text.NativeRendering // or Text.CurveRendering(much more expansive) or Text.QtRendering for faster - textFormat: Text.PlainText - elide: Text.ElideRight - color: Colors.foreground1 - smooth: true + renderType: Text.NativeRendering // or Text.CurveRendering(much more expansive) or Text.QtRendering for faster + textFormat: Text.PlainText + elide: Text.ElideRight + color: Colors.foreground1 + smooth: true - wrapMode: TextEdit.Wrap + wrapMode: TextEdit.Wrap - font { - family: Style.font.family.iosevka - // pointSize: root.pixelSize - pixelSize: fontSize - } + font { + family: Style.font.family.iosevka + // pointSize: root.pixelSize + pixelSize: fontSize + } - onTextChanged: { - if (animate) { - root.scale = animateFrom; - root.opacity = animateFrom; - scaleAnim.to = animateTo; - opacityAnim.to = animateTo; - scaleAnim.easing.bezierCurve = Style.anim.curves.standardAccel; - opacityAnim.easing.bezierCurve = Style.anim.curves.standardAccel; - scaleAnim.start(); - opacityAnim.start(); - } - } + onTextChanged: { + if (animate) { + root.scale = animateFrom; + root.opacity = animateFrom; + scaleAnim.to = animateTo; + opacityAnim.to = animateTo; + scaleAnim.easing.bezierCurve = Style.anim.curves.standardAccel; + opacityAnim.easing.bezierCurve = Style.anim.curves.standardAccel; + scaleAnim.start(); + opacityAnim.start(); + } + } - onLinkActivated: link => { - Qt.openUrlExternally(link); - } + onLinkActivated: link => { + Qt.openUrlExternally(link); + } - Behavior on color { - ColorAnimation { - duration: Style.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Style.anim.curves.standard - } - } + Behavior on color { + ColorAnimation { + duration: Style.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Style.anim.curves.standard + } + } - NumberAnimation { - id: scaleAnim - target: root - property: "scale" - duration: General.animationDuration / 4 / root.multiplier - easing.type: Easing.BezierSpline - } + NumberAnimation { + id: scaleAnim + target: root + property: "scale" + duration: General.animationDuration / 4 / root.multiplier + easing.type: Easing.BezierSpline + } - NumberAnimation { - id: opacityAnim - target: root - property: "opacity" - duration: General.animationDuration / 4 / root.multiplier - easing.type: Easing.BezierSpline - } + NumberAnimation { + id: opacityAnim + target: root + property: "opacity" + duration: General.animationDuration / 4 / root.multiplier + easing.type: Easing.BezierSpline + } } diff --git a/components/ITextEdit.qml b/components/ITextEdit.qml index f05c8c7..7c6dd9e 100644 --- a/components/ITextEdit.qml +++ b/components/ITextEdit.qml @@ -5,53 +5,52 @@ import QtQuick import qs.config TextEdit { - id: root - - property bool animate: false - property real animateFrom: 0 - property real animateTo: 1 - property real fontSize: Style.font.size.larger - - renderType: Text.NativeRendering // or Text.CurveRendering(much more expansive) or Text.QtRendering for faster - textFormat: Text.PlainText - color: Colors.foreground1 - smooth: true - - wrapMode: TextEdit.Wrap - - font { - family: Style.font.family.iosevka - // pointSize: root.pixelSize - pixelSize: fontSize - } - - onTextChanged: { - if (animate) { - root.scale = animateFrom; - scaleAnim.to = animateTo; - scaleAnim.easing.bezierCurve = Style.anim.curves.standardAccel; - scaleAnim.start(); - } - } - - onLinkActivated: (link) => { - Qt.openUrlExternally(link) - } - - Behavior on color { - ColorAnimation { - duration: Style.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Style.anim.curves.standard - } - } - - NumberAnimation { - id: scaleAnim - target: root - property: "scale" - duration: General.animationDuration / 4 - easing.type: Easing.BezierSpline - } - + id: root + + property bool animate: false + property real animateFrom: 0 + property real animateTo: 1 + property real fontSize: Style.font.size.larger + + renderType: Text.NativeRendering // or Text.CurveRendering(much more expansive) or Text.QtRendering for faster + textFormat: Text.PlainText + color: Colors.foreground1 + smooth: true + + wrapMode: TextEdit.Wrap + + font { + family: Style.font.family.iosevka + // pointSize: root.pixelSize + pixelSize: fontSize + } + + onTextChanged: { + if (animate) { + root.scale = animateFrom; + scaleAnim.to = animateTo; + scaleAnim.easing.bezierCurve = Style.anim.curves.standardAccel; + scaleAnim.start(); + } + } + + onLinkActivated: link => { + Qt.openUrlExternally(link); + } + + Behavior on color { + ColorAnimation { + duration: Style.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Style.anim.curves.standard + } + } + + NumberAnimation { + id: scaleAnim + target: root + property: "scale" + duration: General.animationDuration / 4 + easing.type: Easing.BezierSpline + } } diff --git a/components/IWindow.qml b/components/IWindow.qml index b048282..4933e14 100644 --- a/components/IWindow.qml +++ b/components/IWindow.qml @@ -3,12 +3,12 @@ import Quickshell.Wayland import QtQuick PanelWindow { - required property string name - property ShellScreen modelData - property var layer: WlrLayer.Top + required property string name + property ShellScreen modelData + property var layer: WlrLayer.Top - screen: modelData - WlrLayershell.namespace: `${name}` - WlrLayershell.layer: layer - color: "transparent" + screen: modelData + WlrLayershell.namespace: `${name}` + WlrLayershell.layer: layer + color: "transparent" } diff --git a/components/Icon.qml b/components/Icon.qml index a23f6da..3f70807 100644 --- a/components/Icon.qml +++ b/components/Icon.qml @@ -4,24 +4,24 @@ import QtQuick.Layouts import qs.config IText { - id: root - property real iconSize: General.iconSize - property real fill: 0 - property real truncatedFill: Math.round(fill * 100) / 100 // Reduce memory consumption spikes from constant font remapping - renderType: Text.NativeRendering - color: Colors.foreground2 - font { - // hintingPreference: Font.PreferFullHinting - hintingPreference: Font.PreferNoHinting // so the icons dont just offset away - family: Style.font.family.symbols ?? "Symbols Nerd Font" - pixelSize: iconSize - weight: Font.Normal + (Font.DemiBold - Font.Normal) * fill - variableAxes: { - "FILL": truncatedFill, - // "wght": font.weight, - // "GRAD": 0, - "opsz": iconSize - } - } - verticalAlignment: Text.AlignVCenter + id: root + property real iconSize: General.iconSize + property real fill: 0 + property real truncatedFill: Math.round(fill * 100) / 100 // Reduce memory consumption spikes from constant font remapping + renderType: Text.NativeRendering + color: Colors.foreground2 + font { + // hintingPreference: Font.PreferFullHinting + hintingPreference: Font.PreferNoHinting // so the icons dont just offset away + family: Style.font.family.symbols ?? "Symbols Nerd Font" + pixelSize: iconSize + weight: Font.Normal + (Font.DemiBold - Font.Normal) * fill + variableAxes: { + "FILL": truncatedFill, + // "wght": font.weight, + // "GRAD": 0, + "opsz": iconSize + } + } + verticalAlignment: Text.AlignVCenter } diff --git a/components/Searchable.qml b/components/Searchable.qml index a3f1034..666e2d9 100644 --- a/components/Searchable.qml +++ b/components/Searchable.qml @@ -6,259 +6,259 @@ import "../utils/fuzzysort.js" as Fuzzy import "../utils/levendist.js" as Levendist Scope { - required property var list - property string key: "name" - - enum SearchAlgorithm { - Levendist, - Fzf, - Fuzzysort, - Include - } - - property int algorithm: SearchAlgorithm.Levendist - property int scoreThreshold: 50 - property var extraOpts: ({}) - property list<string> keys: [key] - property list<real> weights: [1] - property var queryCache: ({}) - - readonly property var fzf: algorithm === Searchable.SearchAlgorithm.Fzf ? new Fzf.Finder(list, Object.assign({ - selector - }, extraOpts)) : null - - readonly property list<var> fzfFinders: algorithm === Searchable.SearchAlgorithm.Fzf ? keys.map(k => { - return new Fzf.Finder(list, Object.assign({ - selector: item => fieldString(item, k) - }, extraOpts)); - }) : [] - - readonly property list<var> fuzzyPrepped: algorithm === Searchable.SearchAlgorithm.Fuzzysort ? list.map(e => { - const obj = { - _item: e - }; - for (const k of keys) - obj[k] = Fuzzy.prepare(String(e[k] ?? "")); - return obj; - }) : [] - - function transformSearch(search: string): string { - return search; - } - - function fieldString(item: var, k: string): string { - const v = item[k]; - return v == null ? "" : String(v); - } - - function selector(item: var): string { - return fieldString(item, keys[0]); - } - - function getCachedSource(search) { - if (queryCache[search]) - return { - hit: true, - data: queryCache[search] - }; - let source = list; - let previousLists = Object.keys(queryCache).filter(key => key && search.startsWith(key)); - if (previousLists.length > 0) { - var longest = previousLists.reduce((a, b) => a.length >= b.length ? a : b); - source = queryCache[longest]; - } - return { - hit: false, - data: source - }; - } - - function levendistQuery(search: string): list<var> { - const prefixGroups = keys.map(() => []); - const fuzzyGroups = keys.map(() => []); - - for (const s of list) { - let best = null; - for (let i = 0; i < keys.length; i++) { - const t = fieldString(s, keys[i]).toLowerCase(); - if (!t) - continue; - - if (t.startsWith(search)) { - if (!best || i < best.index || best.type !== "prefix") { - best = { - type: "prefix", - index: i, - score: 0 - }; - if (best.index === 0) - break; - } - continue; - } - - let pos = 0; - for (const c of search) { - pos = t.indexOf(c, pos); - if (pos < 0) { - pos = 0; - break; - } - pos++; - } - if (pos) { - const d = Levendist.distance(search, t); - if (d <= scoreThreshold) { - if (!best || i < best.index || (best.type === "fuzzy" && i === best.index && d < best.score) || best.type === "prefix") { - if (!best || i < best.index || d < best.score) { - best = { - type: "fuzzy", - index: i, - score: d - }; - } - } - } - } - } - - if (best) { - if (best.type === "prefix") - prefixGroups[best.index].push(s); - else - fuzzyGroups[best.index].push({ - s, - score: best.score - }); - } - } - - const results = []; - const seen = new Set(); - for (let i = 0; i < keys.length; i++) { - for (const item of prefixGroups[i]) { - if (!seen.has(item)) { - results.push(item); - seen.add(item); - } - } - fuzzyGroups[i].sort((a, b) => a.score - b.score).forEach(({ - s - }) => { - if (!seen.has(s)) { - results.push(s); - seen.add(s); - } - }); - } - - return results; - } - - function fuzzysortQuery(search: string): list<var> { - const res = Fuzzy.go(search, fuzzyPrepped, Object.assign({ - all: true, - keys, - scoreFn: r => weights.reduce((a, w, i) => a + r[i].score * w, 0) - }, extraOpts)); - - const groups = keys.map(() => []); - const noMatch = []; - for (const r of res) { - let idx = -1; - for (let i = 0; i < keys.length; i++) { - const kr = r[i]; - if (kr && kr.score !== -Infinity) { - idx = i; - break; - } - } - if (idx === -1) - noMatch.push(r); - else - groups[idx].push(r); - } - - const ordered = []; - for (let i = 0; i < groups.length; i++) { - for (const r of groups[i]) - ordered.push(r.obj._item); - } - for (const r of noMatch) - ordered.push(r.obj._item); - - return ordered; - } - - function fzfQuery(search: string): list<var> { - let results; - if (keys.length <= 1) { - results = fzf.find(search).sort((a, b) => { - if (a.score === b.score) - return (selector(a.item).trim().length - selector(b.item).trim().length); - return b.score - a.score; - }).map(r => r.item); - } else { - const groups = keys.map((_, i) => { - const arr = fzfFinders[i].find(search); - arr.sort((a, b) => { - if (a.score === b.score) { - const la = fieldString(a.item, keys[i]).trim().length; - const lb = fieldString(b.item, keys[i]).trim().length; - return la - lb; - } - return b.score - a.score; - }); - return arr; - }); - - const seen = new Set(); - const out = []; - for (let i = 0; i < groups.length; i++) { - for (const r of groups[i]) { - const it = r.item; - if (!seen.has(it)) { - out.push(it); - seen.add(it); - } - } - } - results = out; - } - - return results; - } - - function includeQuery(search: string): list<var> { - return list.filter(item => keys.some(k => transformSearch(fieldString(item, k)).includes(search))); - } - - function query(search: string): list<var> { - search = transformSearch(search); - if (!search) - return list; - - const cacheCheck = getCachedSource(search); - if (cacheCheck.hit) - return cacheCheck.data; - - let result; - - if (algorithm === Searchable.SearchAlgorithm.Levendist) - result = levendistQuery(search.toLowerCase()); - - if (algorithm === Searchable.SearchAlgorithm.Fuzzysort) - result = fuzzysortQuery(search); - - if (algorithm === Searchable.SearchAlgorithm.Fzf) - result = fzfQuery(search); - - if (algorithm === Searchable.SearchAlgorithm.Include) - result = includeQuery(search); - - queryCache[search] = result; - - return result; - return []; - } + required property var list + property string key: "name" + + enum SearchAlgorithm { + Levendist, + Fzf, + Fuzzysort, + Include + } + + property int algorithm: SearchAlgorithm.Levendist + property int scoreThreshold: 50 + property var extraOpts: ({}) + property list<string> keys: [key] + property list<real> weights: [1] + property var queryCache: ({}) + + readonly property var fzf: algorithm === Searchable.SearchAlgorithm.Fzf ? new Fzf.Finder(list, Object.assign({ + selector + }, extraOpts)) : null + + readonly property list<var> fzfFinders: algorithm === Searchable.SearchAlgorithm.Fzf ? keys.map(k => { + return new Fzf.Finder(list, Object.assign({ + selector: item => fieldString(item, k) + }, extraOpts)); + }) : [] + + readonly property list<var> fuzzyPrepped: algorithm === Searchable.SearchAlgorithm.Fuzzysort ? list.map(e => { + const obj = { + _item: e + }; + for (const k of keys) + obj[k] = Fuzzy.prepare(String(e[k] ?? "")); + return obj; + }) : [] + + function transformSearch(search: string): string { + return search; + } + + function fieldString(item: var, k: string): string { + const v = item[k]; + return v == null ? "" : String(v); + } + + function selector(item: var): string { + return fieldString(item, keys[0]); + } + + function getCachedSource(search) { + if (queryCache[search]) + return { + hit: true, + data: queryCache[search] + }; + let source = list; + let previousLists = Object.keys(queryCache).filter(key => key && search.startsWith(key)); + if (previousLists.length > 0) { + var longest = previousLists.reduce((a, b) => a.length >= b.length ? a : b); + source = queryCache[longest]; + } + return { + hit: false, + data: source + }; + } + + function levendistQuery(search: string): list<var> { + const prefixGroups = keys.map(() => []); + const fuzzyGroups = keys.map(() => []); + + for (const s of list) { + let best = null; + for (let i = 0; i < keys.length; i++) { + const t = fieldString(s, keys[i]).toLowerCase(); + if (!t) + continue; + + if (t.startsWith(search)) { + if (!best || i < best.index || best.type !== "prefix") { + best = { + type: "prefix", + index: i, + score: 0 + }; + if (best.index === 0) + break; + } + continue; + } + + let pos = 0; + for (const c of search) { + pos = t.indexOf(c, pos); + if (pos < 0) { + pos = 0; + break; + } + pos++; + } + if (pos) { + const d = Levendist.distance(search, t); + if (d <= scoreThreshold) { + if (!best || i < best.index || (best.type === "fuzzy" && i === best.index && d < best.score) || best.type === "prefix") { + if (!best || i < best.index || d < best.score) { + best = { + type: "fuzzy", + index: i, + score: d + }; + } + } + } + } + } + + if (best) { + if (best.type === "prefix") + prefixGroups[best.index].push(s); + else + fuzzyGroups[best.index].push({ + s, + score: best.score + }); + } + } + + const results = []; + const seen = new Set(); + for (let i = 0; i < keys.length; i++) { + for (const item of prefixGroups[i]) { + if (!seen.has(item)) { + results.push(item); + seen.add(item); + } + } + fuzzyGroups[i].sort((a, b) => a.score - b.score).forEach(({ + s + }) => { + if (!seen.has(s)) { + results.push(s); + seen.add(s); + } + }); + } + + return results; + } + + function fuzzysortQuery(search: string): list<var> { + const res = Fuzzy.go(search, fuzzyPrepped, Object.assign({ + all: true, + keys, + scoreFn: r => weights.reduce((a, w, i) => a + r[i].score * w, 0) + }, extraOpts)); + + const groups = keys.map(() => []); + const noMatch = []; + for (const r of res) { + let idx = -1; + for (let i = 0; i < keys.length; i++) { + const kr = r[i]; + if (kr && kr.score !== -Infinity) { + idx = i; + break; + } + } + if (idx === -1) + noMatch.push(r); + else + groups[idx].push(r); + } + + const ordered = []; + for (let i = 0; i < groups.length; i++) { + for (const r of groups[i]) + ordered.push(r.obj._item); + } + for (const r of noMatch) + ordered.push(r.obj._item); + + return ordered; + } + + function fzfQuery(search: string): list<var> { + let results; + if (keys.length <= 1) { + results = fzf.find(search).sort((a, b) => { + if (a.score === b.score) + return (selector(a.item).trim().length - selector(b.item).trim().length); + return b.score - a.score; + }).map(r => r.item); + } else { + const groups = keys.map((_, i) => { + const arr = fzfFinders[i].find(search); + arr.sort((a, b) => { + if (a.score === b.score) { + const la = fieldString(a.item, keys[i]).trim().length; + const lb = fieldString(b.item, keys[i]).trim().length; + return la - lb; + } + return b.score - a.score; + }); + return arr; + }); + + const seen = new Set(); + const out = []; + for (let i = 0; i < groups.length; i++) { + for (const r of groups[i]) { + const it = r.item; + if (!seen.has(it)) { + out.push(it); + seen.add(it); + } + } + } + results = out; + } + + return results; + } + + function includeQuery(search: string): list<var> { + return list.filter(item => keys.some(k => transformSearch(fieldString(item, k)).includes(search))); + } + + function query(search: string): list<var> { + search = transformSearch(search); + if (!search) + return list; + + const cacheCheck = getCachedSource(search); + if (cacheCheck.hit) + return cacheCheck.data; + + let result; + + if (algorithm === Searchable.SearchAlgorithm.Levendist) + result = levendistQuery(search.toLowerCase()); + + if (algorithm === Searchable.SearchAlgorithm.Fuzzysort) + result = fuzzysortQuery(search); + + if (algorithm === Searchable.SearchAlgorithm.Fzf) + result = fzfQuery(search); + + if (algorithm === Searchable.SearchAlgorithm.Include) + result = includeQuery(search); + + queryCache[search] = result; + + return result; + return []; + } } diff --git a/config/Bar.qml b/config/Bar.qml index 5506fa0..d02e006 100644 --- a/config/Bar.qml +++ b/config/Bar.qml @@ -3,43 +3,43 @@ pragma Singleton import Quickshell Singleton { - id: root + id: root - readonly property int height: 40 - readonly property int borderRadius: Style.rounding.small - readonly property int topMargin: Style.spacing.small - readonly property int leftMargin: Style.spacing.small - readonly property int rightMargin: Style.spacing.small + readonly property int height: 40 + readonly property int borderRadius: Style.rounding.small + readonly property int topMargin: Style.spacing.small + readonly property int leftMargin: Style.spacing.small + readonly property int rightMargin: Style.spacing.small - readonly property int moduleLeftMargin: Style.spacing.larger - readonly property int moduleRightMargin: Style.spacing.small + readonly property int moduleLeftMargin: Style.spacing.larger + readonly property int moduleRightMargin: Style.spacing.small - readonly property int leftModuleSpacing: Style.spacing.smaller - readonly property int rightModuleSpacing: Style.spacing.smaller + readonly property int leftModuleSpacing: Style.spacing.smaller + readonly property int rightModuleSpacing: Style.spacing.smaller - readonly property var windowStrip: [" — Zen Browser"] + readonly property var windowStrip: [" — Zen Browser"] - readonly property int wss: 10 - readonly property int wsIconSize: 32 - readonly property int wsSpacing: 2 - readonly property int wsHorizontalSpacing: Style.spacing.smaller - readonly property int wsActiveIconSize: wsIconSize * 1.5 - readonly property int wsRounding: Style.rounding.small - readonly property double wsOpacity: 0.8 - readonly property int wsActiveOpacity: 1 - readonly property double wsEmptyOpacity: 0.2 - // wsAnimationEasing using Hyprland's actual ws animation easing - readonly property int wsAnimationDuration: 200 - readonly property int wsExtraMouseArea: wsHorizontalSpacing / 2 + readonly property int wss: 10 + readonly property int wsIconSize: 32 + readonly property int wsSpacing: 2 + readonly property int wsHorizontalSpacing: Style.spacing.smaller + readonly property int wsActiveIconSize: wsIconSize * 1.5 + readonly property int wsRounding: Style.rounding.small + readonly property double wsOpacity: 0.8 + readonly property int wsActiveOpacity: 1 + readonly property double wsEmptyOpacity: 0.2 + // wsAnimationEasing using Hyprland's actual ws animation easing + readonly property int wsAnimationDuration: 200 + readonly property int wsExtraMouseArea: wsHorizontalSpacing / 2 - readonly property int resourceIconTextSpacing: Style.spacing.small + readonly property int resourceIconTextSpacing: Style.spacing.small - readonly property int moduleRadius: Style.rounding.small + readonly property int moduleRadius: Style.rounding.small - readonly property double mediaPausedOpacity: 0.75 - readonly property double mediaScrollScale: 1 / 25 // 5sec - readonly property double hoverScale: 1.2 - readonly property string preferedPlayer: "spotify" + readonly property double mediaPausedOpacity: 0.75 + readonly property double mediaScrollScale: 1 / 25 // 5sec + readonly property double hoverScale: 1.2 + readonly property string preferedPlayer: "spotify" - readonly property double bgTransparency: 0.25 + readonly property double bgTransparency: 0.25 } diff --git a/config/Colors.qml b/config/Colors.qml index 259ec73..e223ca6 100644 --- a/config/Colors.qml +++ b/config/Colors.qml @@ -3,44 +3,44 @@ import Quickshell import QtQuick Singleton { - readonly property color background1: WallustColors.color8 - // readonly property color background2: WallustColors.color0 - readonly property color background2: Qt.rgba(WallustColors.color0.r, WallustColors.color0.g, WallustColors.color0.b, General.accentTransparency) - readonly property color background3: WallustColors.background - readonly property color a1: WallustColors.color9 - readonly property color a2: WallustColors.color1 - readonly property color b1: WallustColors.color10 - readonly property color b2: WallustColors.color2 - readonly property color c1: WallustColors.color11 - readonly property color c2: WallustColors.color3 - readonly property color d1: WallustColors.color12 - readonly property color d2: WallustColors.color4 - readonly property color e1: WallustColors.color13 - readonly property color e2: WallustColors.color5 - readonly property color f1: WallustColors.color14 - readonly property color f2: WallustColors.color6 - readonly property color foreground1: WallustColors.foreground - readonly property color foreground2: WallustColors.color15 - readonly property color foreground3: WallustColors.color7 - readonly property color cursor: WallustColors.cursor + readonly property color background1: WallustColors.color8 + // readonly property color background2: WallustColors.color0 + readonly property color background2: Qt.rgba(WallustColors.color0.r, WallustColors.color0.g, WallustColors.color0.b, General.accentTransparency) + readonly property color background3: WallustColors.background + readonly property color a1: WallustColors.color9 + readonly property color a2: WallustColors.color1 + readonly property color b1: WallustColors.color10 + readonly property color b2: WallustColors.color2 + readonly property color c1: WallustColors.color11 + readonly property color c2: WallustColors.color3 + readonly property color d1: WallustColors.color12 + readonly property color d2: WallustColors.color4 + readonly property color e1: WallustColors.color13 + readonly property color e2: WallustColors.color5 + readonly property color f1: WallustColors.color14 + readonly property color f2: WallustColors.color6 + readonly property color foreground1: WallustColors.foreground + readonly property color foreground2: WallustColors.color15 + readonly property color foreground3: WallustColors.color7 + readonly property color cursor: WallustColors.cursor - readonly property color accent: background2 - readonly property color accentAlt: background1 + readonly property color accent: background2 + readonly property color accentAlt: background1 - readonly property color black: Qt.rgba(59 / 255, 66 / 255, 82 / 255, 1) - readonly property color red: Qt.rgba(191 / 255, 97 / 255, 106 / 255, 1) - readonly property color green: Qt.rgba(163 / 255, 190 / 255, 140 / 255, 1) - readonly property color yellow: Qt.rgba(235 / 255, 203 / 255, 139 / 255, 1) - readonly property color blue: Qt.rgba(129 / 255, 161 / 255, 193 / 255, 1) - readonly property color magenta: Qt.rgba(180 / 255, 142 / 255, 173 / 255, 1) - readonly property color cyan: Qt.rgba(136 / 255, 192 / 255, 208 / 255, 1) - readonly property color white: Qt.rgba(229 / 255, 233 / 255, 240 / 255, 1) - readonly property color altBlack: Qt.rgba(76 / 255, 86 / 255, 106 / 255, 1) - readonly property color altRed: Qt.rgba(191 / 255, 97 / 255, 106 / 255, 1) - readonly property color altGreen: Qt.rgba(163 / 255, 190 / 255, 140 / 255, 1) - readonly property color altYellow: Qt.rgba(235 / 255, 203 / 255, 139 / 255, 1) - readonly property color altBlue: Qt.rgba(129 / 255, 161 / 255, 193 / 255, 1) - readonly property color altMagenta: Qt.rgba(180 / 255, 142 / 255, 173 / 255, 1) - readonly property color altCyan: Qt.rgba(143 / 255, 188 / 255, 187 / 255, 1) - readonly property color altWhite: Qt.rgba(236 / 255, 239 / 255, 244 / 255, 1) + readonly property color black: Qt.rgba(59 / 255, 66 / 255, 82 / 255, 1) + readonly property color red: Qt.rgba(191 / 255, 97 / 255, 106 / 255, 1) + readonly property color green: Qt.rgba(163 / 255, 190 / 255, 140 / 255, 1) + readonly property color yellow: Qt.rgba(235 / 255, 203 / 255, 139 / 255, 1) + readonly property color blue: Qt.rgba(129 / 255, 161 / 255, 193 / 255, 1) + readonly property color magenta: Qt.rgba(180 / 255, 142 / 255, 173 / 255, 1) + readonly property color cyan: Qt.rgba(136 / 255, 192 / 255, 208 / 255, 1) + readonly property color white: Qt.rgba(229 / 255, 233 / 255, 240 / 255, 1) + readonly property color altBlack: Qt.rgba(76 / 255, 86 / 255, 106 / 255, 1) + readonly property color altRed: Qt.rgba(191 / 255, 97 / 255, 106 / 255, 1) + readonly property color altGreen: Qt.rgba(163 / 255, 190 / 255, 140 / 255, 1) + readonly property color altYellow: Qt.rgba(235 / 255, 203 / 255, 139 / 255, 1) + readonly property color altBlue: Qt.rgba(129 / 255, 161 / 255, 193 / 255, 1) + readonly property color altMagenta: Qt.rgba(180 / 255, 142 / 255, 173 / 255, 1) + readonly property color altCyan: Qt.rgba(143 / 255, 188 / 255, 187 / 255, 1) + readonly property color altWhite: Qt.rgba(236 / 255, 239 / 255, 244 / 255, 1) } diff --git a/config/General.qml b/config/General.qml index a9f9929..77bc177 100644 --- a/config/General.qml +++ b/config/General.qml @@ -4,29 +4,29 @@ import Quickshell import qs.components Singleton { - id: root + id: root - readonly property int resourceUpdateInterval: 1 + readonly property int resourceUpdateInterval: 1 - readonly property real iconSize: Style.font.size.larger + readonly property real iconSize: Style.font.size.larger - readonly property string cpuThermalPath: "/sys/class/thermal/thermal_zone0/temp" + readonly property string cpuThermalPath: "/sys/class/thermal/thermal_zone0/temp" - readonly property int rectMargin: Style.spacing.small + readonly property int rectMargin: Style.spacing.small - readonly property int appIconSize: 25 + readonly property int appIconSize: 25 - readonly property int appSearchFuzzySearchThreshold: 5 + readonly property int appSearchFuzzySearchThreshold: 5 - readonly property int fontSize: Style.font.size.larger + readonly property int fontSize: Style.font.size.larger - readonly property int animationDuration: Style.anim.durations.normal - readonly property real springAnimationSpring: 8 - readonly property real springAnimationDamping: 0.4 + readonly property int animationDuration: Style.anim.durations.normal + readonly property real springAnimationSpring: 8 + readonly property real springAnimationDamping: 0.4 - readonly property double accentTransparency: 0.15 + readonly property double accentTransparency: 0.15 - readonly property int defaultSearchAlgorithm: Searchable.SearchAlgorithm.Include + readonly property int defaultSearchAlgorithm: Searchable.SearchAlgorithm.Include - readonly property int maxClipPreviewChar: 100 + readonly property int maxClipPreviewChar: 100 } diff --git a/config/Icons.qml b/config/Icons.qml index 240be6b..f2a80ff 100644 --- a/config/Icons.qml +++ b/config/Icons.qml @@ -3,67 +3,67 @@ pragma Singleton import Quickshell Singleton { - id: root + id: root - readonly property var ws: ({ - 1: "", - 2: "", - 3: "", - 4: "", - 5: "", - 6: "", - 7: "", - 8: "", - 9: "", - 10: "", - urgent: "", - active: "", - default: " " - }) + readonly property var ws: ({ + 1: "", + 2: "", + 3: "", + 4: "", + 5: "", + 6: "", + 7: "", + 8: "", + 9: "", + 10: "", + urgent: "", + active: "", + default: " " + }) - readonly property var media: ({ - play: "", - pause: "", - stop: "", - // next: "", - // prev: "", - mute: "", - unmute: "" - }) + readonly property var media: ({ + play: "", + pause: "", + stop: "", + // next: "", + // prev: "", + mute: "", + unmute: "" + }) - readonly property var wifi: ({ - min: "", - low: "", - mid: "", - high: "", - max: "" - }) + readonly property var wifi: ({ + min: "", + low: "", + mid: "", + high: "", + max: "" + }) - readonly property var network: ({ - disconnected: "", - download: "", - upload: "", - wifi: wifi - }) + readonly property var network: ({ + disconnected: "", + download: "", + upload: "", + wifi: wifi + }) - readonly property list<string> clock: ['', '', '', '', '', '', '', '', '', '', '', ''] + readonly property list<string> clock: ['', '', '', '', '', '', '', '', '', '', '', ''] - readonly property var resource: ({ - cpu: "", - memory: "", - disk: "", - gpu: "", - temp: "", - network: network, - clock: clock, - calendar: "" - }) + readonly property var resource: ({ + cpu: "", + memory: "", + disk: "", + gpu: "", + temp: "", + network: network, + clock: clock, + calendar: "" + }) - readonly property var power: ({ - shutdown: "", - dpms: "", - lock: "", - suspend: "", - reboot: "" - }) + readonly property var power: ({ + shutdown: "", + dpms: "", + lock: "", + suspend: "", + reboot: "" + }) } diff --git a/config/Launcher.qml b/config/Launcher.qml index d93b8a7..5df5c85 100644 --- a/config/Launcher.qml +++ b/config/Launcher.qml @@ -5,66 +5,66 @@ import QtQuick import qs.popups.launcher.components Singleton { - id: root + id: root - readonly property int defaultWidth: 500 + readonly property int defaultWidth: 500 - readonly property int widgetWidth: 700 - readonly property int widgetHeight: 200 + readonly property int widgetWidth: 700 + readonly property int widgetHeight: 200 - readonly property int innerMargin: 5 + readonly property int innerMargin: 5 - readonly property int borderRadius: Style.rounding.large - readonly property int borderWidth: 1 + readonly property int borderRadius: Style.rounding.large + readonly property int borderWidth: 1 - readonly property int widgetRadius: Style.rounding.small - readonly property int widgetFontSize: Style.font.size.large + readonly property int widgetRadius: Style.rounding.small + readonly property int widgetFontSize: Style.font.size.large - readonly property double bgTransparency: 0.4 - readonly property double widgetBgTransparency: 0.3 - readonly property double widgetTitleBgTransparency: 0.2 // as it's on top of it + readonly property double bgTransparency: 0.4 + readonly property double widgetBgTransparency: 0.3 + readonly property double widgetTitleBgTransparency: 0.2 // as it's on top of it - readonly property bool showWidgetTitle: false + readonly property bool showWidgetTitle: false - readonly property int topMargin: 400 + readonly property int topMargin: 400 - readonly property int predictiveCompletionRadius: 6 + readonly property int predictiveCompletionRadius: 6 - readonly property int maxPreviewLen: 20 + readonly property int maxPreviewLen: 20 - readonly property int duckduckgoSpacing: 25 + readonly property int duckduckgoSpacing: 25 - readonly property list<Component> widgets: [ - // recommanded order: - // 1. components with .startswith - // 2. utils - // 3. app launcher - Component { - Emoji {} - }, - Component { - Clip {} - }, - Component { - Spell {} - }, - Component { - Calc {} - }, - Component { - DateTime {} - }, - Component { - Unit {} - }, - Component { - Currency {} - }, - Component { - App {} - }, - Component { - DuckDuckGo {} - } - ] + readonly property list<Component> widgets: [ + // recommanded order: + // 1. components with .startswith + // 2. utils + // 3. app launcher + Component { + Emoji {} + }, + Component { + Clip {} + }, + Component { + Spell {} + }, + Component { + Calc {} + }, + Component { + DateTime {} + }, + Component { + Unit {} + }, + Component { + Currency {} + }, + Component { + App {} + }, + Component { + DuckDuckGo {} + } + ] } diff --git a/config/Style.qml b/config/Style.qml index 25bf6dc..2ec507a 100644 --- a/config/Style.qml +++ b/config/Style.qml @@ -4,85 +4,85 @@ import Quickshell import QtQuick Singleton { - id: root + id: root - readonly property Rounding rounding: Rounding {} - readonly property Spacing spacing: Spacing {} - readonly property Padding padding: Padding {} - readonly property Font font: Font {} - readonly property Anim anim: Anim {} + readonly property Rounding rounding: Rounding {} + readonly property Spacing spacing: Spacing {} + readonly property Padding padding: Padding {} + readonly property Font font: Font {} + readonly property Anim anim: Anim {} - component Rounding: QtObject { - readonly property int smaller: 10 - readonly property int small: 12 - readonly property int normal: 17 - readonly property int large: 25 - readonly property int full: 1000 - } + component Rounding: QtObject { + readonly property int smaller: 10 + readonly property int small: 12 + readonly property int normal: 17 + readonly property int large: 25 + readonly property int full: 1000 + } - component Spacing: QtObject { - readonly property int small: 7 - readonly property int smaller: 10 - readonly property int normal: 12 - readonly property int larger: 15 - readonly property int largerr: 17 - readonly property int large: 20 - } + component Spacing: QtObject { + readonly property int small: 7 + readonly property int smaller: 10 + readonly property int normal: 12 + readonly property int larger: 15 + readonly property int largerr: 17 + readonly property int large: 20 + } - component Padding: QtObject { - readonly property int small: 5 - readonly property int smaller: 7 - readonly property int normal: 10 - readonly property int larger: 12 - readonly property int large: 15 - } + component Padding: QtObject { + readonly property int small: 5 + readonly property int smaller: 7 + readonly property int normal: 10 + readonly property int larger: 12 + readonly property int large: 15 + } - component FontFamily: QtObject { - readonly property string sans: "SF Pro Text" - readonly property string symbols: "Symbols Nerd Font Mono" - readonly property string iosevka: "Iosevka Nerd Font" - readonly property string mono: "JetBrains Mono" - } + component FontFamily: QtObject { + readonly property string sans: "SF Pro Text" + readonly property string symbols: "Symbols Nerd Font Mono" + readonly property string iosevka: "Iosevka Nerd Font" + readonly property string mono: "JetBrains Mono" + } - component FontSize: QtObject { - readonly property int small: 11 - readonly property int smaller: 12 - readonly property int normal: 13 - readonly property int larger: 15 - readonly property int largerr: 17 - readonly property int large: 18 - readonly property int extraLarge: 28 - } + component FontSize: QtObject { + readonly property int small: 11 + readonly property int smaller: 12 + readonly property int normal: 13 + readonly property int larger: 15 + readonly property int largerr: 17 + readonly property int large: 18 + readonly property int extraLarge: 28 + } - component Font: QtObject { - readonly property FontFamily family: FontFamily {} - readonly property FontSize size: FontSize {} - } + component Font: QtObject { + readonly property FontFamily family: FontFamily {} + readonly property FontSize size: FontSize {} + } - component AnimCurves: QtObject { - readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] - readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] - readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] - readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1] - readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1] - readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1] - readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] - readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] - readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] - } + component AnimCurves: QtObject { + readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1] + readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1] + readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1] + readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] + readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + } - component AnimDurations: QtObject { - readonly property int small: 200 - readonly property int normal: 400 - readonly property int large: 600 - readonly property int extraLarge: 1000 - readonly property int expressiveFastSpatial: 350 - readonly property int expressiveDefaultSpatial: 500 - readonly property int expressiveEffects: 200 - } + component AnimDurations: QtObject { + readonly property int small: 200 + readonly property int normal: 400 + readonly property int large: 600 + readonly property int extraLarge: 1000 + readonly property int expressiveFastSpatial: 350 + readonly property int expressiveDefaultSpatial: 500 + readonly property int expressiveEffects: 200 + } - component Anim: QtObject { - readonly property AnimCurves curves: AnimCurves {} - readonly property AnimDurations durations: AnimDurations {} - } + component Anim: QtObject { + readonly property AnimCurves curves: AnimCurves {} + readonly property AnimDurations durations: AnimDurations {} + } } diff --git a/popups/IPopup.qml b/popups/IPopup.qml index 59d569d..84b8fd7 100644 --- a/popups/IPopup.qml +++ b/popups/IPopup.qml @@ -1,18 +1,17 @@ import Quickshell.Wayland - import qs.components IWindow { - required property var parentLoader - // TODO: optional only display on focused screen - focusable: true + required property var parentLoader - layer: WlrLayer.Overlay + // TODO: optional only display on focused screen + focusable: true + layer: WlrLayer.Overlay - anchors { - top: false - bottom: false - left: false - right: false - } + anchors { + top: false + bottom: false + left: false + right: false + } } diff --git a/popups/launcher/ILauncher.qml b/popups/launcher/ILauncher.qml index 325ab2c..e268b26 100644 --- a/popups/launcher/ILauncher.qml +++ b/popups/launcher/ILauncher.qml @@ -3,13 +3,13 @@ import Quickshell.Hyprland import ".." IPopup { - id: root + id: root - HyprlandFocusGrab { - active: true - windows: [root] - onCleared: { - parentLoader.active = false; // or active = true again - } - } + HyprlandFocusGrab { + active: true + windows: [root] + onCleared: { + parentLoader.active = false; // or active = true again + } + } } diff --git a/popups/launcher/Launcher.qml b/popups/launcher/Launcher.qml index fd6b851..c198603 100644 --- a/popups/launcher/Launcher.qml +++ b/popups/launcher/Launcher.qml @@ -8,454 +8,454 @@ import qs.config // TODO: currently, we're relying on the compositor to provide the animation which ig is fine, but we should use our own in the future, however it might look weird combined with the compositors's ILauncher { - id: launcher - - property list<Component> widgetItems: Launcher.widgets - property var state: SelectionState - - name: "quickshell::launcher::launcher" - - anchors.top: true - margins.top: Launcher.topMargin - aboveWindows: true - exclusionMode: ExclusionMode.Ignore - - IRect { - id: container - property var selection: launcher.state?.selected - property var mappedSelection: selection?.mapToItem(null, 0, 0) - - onMappedSelectionChanged: { - // launcher.state?.selected may be slow.. smh qt - // my guess is mapToItem is called when its not finished doing whatever it needs to do, - // so it doesnt get the right vars, but it doesn't update after since it already triggered it - selectionSync.running = true; - } - - // for some reason selection changing doesnt call onMappedSelectionChanged, so we manually trigger it - onSelectionChanged: { - selectionSync.running = true; - } - - Timer { - id: selectionSync - // TODO: evaluate weather i just leave it running repeat, might be costy though - running: false - repeat: false - // sync w/ IComponent y animation duration - interval: General.animationDuration / 4 - onTriggered: { - parent.mappedSelection = launcher.state?.selected?.mapToItem(null, 0, 0); - } - } - - IRect { - id: activeRect - visible: parent.mappedSelection || false - y: parent.mappedSelection?.y || 0 - x: parent.mappedSelection?.x || 0 - implicitWidth: launcher.state?.selected?.width || 0 - implicitHeight: launcher.state?.selected?.height || 0 - radius: Launcher.borderRadius - color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.bgTransparency) - Behavior on x { - ISpringAnimation { - id: springAnim - speed: 1.5 - } - } - Behavior on y { - ISpringAnimation { - speed: 1.5 - } - } - Behavior on implicitWidth { - NumberAnimation { - duration: springAnim.duration - easing.type: Easing.BezierSpline - // fast slow - easing.bezierCurve: [0.33, 0.8, 0, 1] - } - } - Behavior on implicitHeight { - NumberAnimation { - duration: springAnim.duration - easing.type: Easing.BezierSpline - // fast slow - easing.bezierCurve: [0.33, 0.8, 0, 1] - } - } - } - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.bgTransparency) - radius: Launcher.borderRadius - - width: Math.max(root.width, Launcher.defaultWidth) + Launcher.innerMargin - height: root.height - - IRect { - id: root - property alias input: textInput.text - - height: searchBar.height + widgets.height + textInput.height / 2 // * 1.5 - width: Math.max(widgets.implicitWidth, Launcher.defaultWidth) - - anchors.centerIn: parent - - color: "transparent" - - RowLayout { - id: searchBar - anchors { - top: parent.top - left: parent.left - right: parent.right - leftMargin: Launcher.innerMargin - rightMargin: Launcher.innerMargin - // try to center it when no widgets are present - topMargin: widgets.height === Launcher.widgets.length * 5 - 5 ? parent.height / 2 - textInput.height / 2 : Launcher.innerMargin // textInput or Icon is the height of this - - Behavior on topMargin { - ISpringAnimation {} - } - } - - spacing: Launcher.innerMargin * 2 - - Icon { - text: "" - } - - RowLayout { - spacing: 0 - TextInput { - id: textInput - property bool pendingUpdate: false - Keys.onPressed: event => { - if (event.key === Qt.Key_End || event.key === Qt.Key_Home) { - if (event.key === Qt.Key_End) { - endShortcut.activated(); - } else if (event.key === Qt.Key_Home) { - homeShortcut.activated(); - } - event.accepted = true; - } - } - - Layout.fillHeight: true - Layout.fillWidth: true - Layout.maximumWidth: implicitWidth - - color: Colors.foreground1 - selectionColor: Colors.background3 - - text: launcher.state?.input || "" - clip: true - focus: true - - antialiasing: true - smooth: true - font { - pixelSize: Style.font.size.large - family: Style.font.family.sans - wordSpacing: 5 - } - // move to different file if we have more textinput - cursorDelegate: IRect { - id: cursor - color: Colors.cursor - // width: 1.5 - width: 2 - height: 1 - // blinking animation - SequentialAnimation { - running: true - loops: Animation.Infinite - PauseAnimation { - duration: 500 - } - ParallelAnimation { - PropertyAnimation { - target: cursor - property: "opacity" - to: 0 - duration: General.animationDuration / 5 - } - PropertyAnimation { - target: cursor - property: "scale" - to: 0 - duration: General.animationDuration / 5 - } - } - PauseAnimation { - duration: 500 - } - ParallelAnimation { - PropertyAnimation { - target: cursor - property: "opacity" - to: 1 - duration: General.animationDuration / 5 - } - PropertyAnimation { - target: cursor - property: "scale" - to: 1 - duration: General.animationDuration / 5 - } - } - } - } - HoverHandler { - cursorShape: Qt.IBeamCursor - } - onTextChanged: { - if (launcher.state.input !== textInput.text) - pendingUpdate = true; - - Qt.callLater(() => { - if (pendingUpdate) { - pendingUpdate = false; - launcher.state.input = textInput.text; // TODO: find ways to optimize this, like how i was using an alias - } - }); - } - onCursorPositionChanged: { - launcher.state.cursorPosition = textInput.cursorPosition; - } - // onActiveFocusChanged: { - // parentLoader.active = activeFocus; - // } - onAccepted: { - launcher.state?.priorities[launcher.state.selectedPriority]?._exec(); // TODO: kde like waiting animation for app to launch - parentLoader.active = false; - textInput.text = ""; - } - Component.onCompleted: { - selectAll(); - } - } - IRect { - opacity: launcher.state?.priorities[launcher.state.selectedPriority]?.predictiveCompletion ? 1 : 0 - Layout.fillHeight: true - Layout.preferredWidth: text.width - radius: Launcher.predictiveCompletionRadius - color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) - IText { - id: text - // animate: true // too jumpy - color: Colors.foreground3 - renderType: Text.CurveRendering - antialiasing: true - smooth: true - font { - pixelSize: Style.font.size.large - family: Style.font.family.sans - wordSpacing: 5 - } - text: launcher.state?.priorities[launcher.state.selectedPriority]?.predictiveCompletion || "" - } - Behavior on Layout.preferredWidth { - ISpringAnimation {} - // NumberAnimation { - // duration: General.animationDuration / 5 - // } - } - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 2 - easing.type: Easing.InOutQuad - } - } - } - Item { - Layout.fillWidth: true - } - Loader { - active: launcher.state?.priorities[launcher.state.selectedPriority]?.priority || false // .priority should always be true if it's in priorities - sourceComponent: launcher.state?.priorities[launcher.state.selectedPriority]?.preview - } - } - } - - ColumnLayout { - id: widgets - anchors { - top: searchBar.bottom - left: parent.left - right: parent.right - topMargin: Launcher.innerMargin * 1.5 // gap in between, maybe seperator FIXME - } - - Repeater { - model: launcher.widgetItems - delegate: Loader { - active: true - visible: item?.valid ?? true // not item?.standalone cuz when its standalone it doesnt appear in launcher.widgetItems - sourceComponent: modelData - asynchronous: true - onLoaded: { - launcher.state.widgets[index] = item; - // FIXME: prev predictiveCompletion still exists here - // workaround atm: just clear it or preserve the text manually - } - - Connections { - target: item - ignoreUnknownSignals: true - - function onClose() { - parentLoader.active = false; - } - } - } - } - } - } - - // not Keys.onPressed because textinput will steal it - Shortcut { - sequence: "Escape" - context: Qt.ApplicationShortcut - onActivated: parentLoader.active = false - } - - Shortcut { - sequence: "Ctrl+H" - context: Qt.ApplicationShortcut - onActivated: changePos(1) - } - - Shortcut { - sequence: "Ctrl+J" - context: Qt.ApplicationShortcut - onActivated: changePos(-2) - } - - Shortcut { - sequence: "Ctrl+K" - context: Qt.ApplicationShortcut - onActivated: changePos(2) - } - - Shortcut { - sequence: "Ctrl+L" - context: Qt.ApplicationShortcut - onActivated: changePos(-1) - } - - Shortcut { - sequence: "Down" - context: Qt.ApplicationShortcut - onActivated: changePos(-2) - } - - Shortcut { - sequence: "Up" - context: Qt.ApplicationShortcut - onActivated: changePos(2) - } - - Shortcut { - sequence: "PgDown" - context: Qt.ApplicationShortcut - onActivated: changePos(-20) - } - - Shortcut { - sequence: "PgUp" - context: Qt.ApplicationShortcut - onActivated: changePos(20) - } - - Shortcut { - id: homeShortcut - sequence: "Home" - context: Qt.ApplicationShortcut - onActivated: changePos(200) - } - - Shortcut { - id: endShortcut - sequence: StandardKey.MoveToEndOfDocument - context: Qt.ApplicationShortcut - onActivated: changePos(-200) - } - } - // 1 = left - // -1 = right - // 2 = up - // -2 = down - function changePos(direction: int) { - const item = launcher.state?.priorities[launcher.state.selectedPriority]; - if (!item) - return; - - let set = 0; - - switch (direction) { - case 1: - if (item.prev()) - set = -1; - break; - case -1: - if (item.next()) - set = 1; - break; - case 2: - if (item.up()) - set = -1; - break; - case -2: - if (item.down()) - set = 1; - break; - case 20: - if (item.pgup()) - set = -1; - break; - case -20: - if (item.pgdn()) - set = 1; - break; - case 200: - if (item.home()) - set = -1; - break; - case -200: - if (item.end()) - set = 1; - break; - } - - if (set !== 0) { - switch (set) { - case 1: - if (launcher.state.selectedPriority < launcher.state.priorities.length - 1) { - launcher.state.selectedPriority++; - launcher.state.priorities[launcher.state.selectedPriority].home(); - } - break; - case -1: - if (launcher.state.selectedPriority > 0) { - launcher.state.selectedPriority--; - launcher.state.priorities[launcher.state.selectedPriority].end(); - } - break; - } - } - } - - implicitWidth: container.width - implicitHeight: container.height - - Connections { - target: launcher?.state - // launcher.state may be null - ignoreUnknownSignals: true - - function onSyncSelectionState() { - selectionSync.running = true; - } - } + id: launcher + + property list<Component> widgetItems: Launcher.widgets + property var state: SelectionState + + name: "quickshell::launcher::launcher" + + anchors.top: true + margins.top: Launcher.topMargin + aboveWindows: true + exclusionMode: ExclusionMode.Ignore + + IRect { + id: container + property var selection: launcher.state?.selected + property var mappedSelection: selection?.mapToItem(null, 0, 0) + + onMappedSelectionChanged: { + // launcher.state?.selected may be slow.. smh qt + // my guess is mapToItem is called when its not finished doing whatever it needs to do, + // so it doesnt get the right vars, but it doesn't update after since it already triggered it + selectionSync.running = true; + } + + // for some reason selection changing doesnt call onMappedSelectionChanged, so we manually trigger it + onSelectionChanged: { + selectionSync.running = true; + } + + Timer { + id: selectionSync + // TODO: evaluate weather i just leave it running repeat, might be costy though + running: false + repeat: false + // sync w/ IComponent y animation duration + interval: General.animationDuration / 4 + onTriggered: { + parent.mappedSelection = launcher.state?.selected?.mapToItem(null, 0, 0); + } + } + + IRect { + id: activeRect + visible: parent.mappedSelection || false + y: parent.mappedSelection?.y || 0 + x: parent.mappedSelection?.x || 0 + implicitWidth: launcher.state?.selected?.width || 0 + implicitHeight: launcher.state?.selected?.height || 0 + radius: Launcher.borderRadius + color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.bgTransparency) + Behavior on x { + ISpringAnimation { + id: springAnim + speed: 1.5 + } + } + Behavior on y { + ISpringAnimation { + speed: 1.5 + } + } + Behavior on implicitWidth { + NumberAnimation { + duration: springAnim.duration + easing.type: Easing.BezierSpline + // fast slow + easing.bezierCurve: [0.33, 0.8, 0, 1] + } + } + Behavior on implicitHeight { + NumberAnimation { + duration: springAnim.duration + easing.type: Easing.BezierSpline + // fast slow + easing.bezierCurve: [0.33, 0.8, 0, 1] + } + } + } + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.bgTransparency) + radius: Launcher.borderRadius + + width: Math.max(root.width, Launcher.defaultWidth) + Launcher.innerMargin + height: root.height + + IRect { + id: root + property alias input: textInput.text + + height: searchBar.height + widgets.height + textInput.height / 2 // * 1.5 + width: Math.max(widgets.implicitWidth, Launcher.defaultWidth) + + anchors.centerIn: parent + + color: "transparent" + + RowLayout { + id: searchBar + anchors { + top: parent.top + left: parent.left + right: parent.right + leftMargin: Launcher.innerMargin + rightMargin: Launcher.innerMargin + // try to center it when no widgets are present + topMargin: widgets.height === Launcher.widgets.length * 5 - 5 ? parent.height / 2 - textInput.height / 2 : Launcher.innerMargin // textInput or Icon is the height of this + + Behavior on topMargin { + ISpringAnimation {} + } + } + + spacing: Launcher.innerMargin * 2 + + Icon { + text: "" + } + + RowLayout { + spacing: 0 + TextInput { + id: textInput + property bool pendingUpdate: false + Keys.onPressed: event => { + if (event.key === Qt.Key_End || event.key === Qt.Key_Home) { + if (event.key === Qt.Key_End) { + endShortcut.activated(); + } else if (event.key === Qt.Key_Home) { + homeShortcut.activated(); + } + event.accepted = true; + } + } + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + + color: Colors.foreground1 + selectionColor: Colors.background3 + + text: launcher.state?.input || "" + clip: true + focus: true + + antialiasing: true + smooth: true + font { + pixelSize: Style.font.size.large + family: Style.font.family.sans + wordSpacing: 5 + } + // move to different file if we have more textinput + cursorDelegate: IRect { + id: cursor + color: Colors.cursor + // width: 1.5 + width: 2 + height: 1 + // blinking animation + SequentialAnimation { + running: true + loops: Animation.Infinite + PauseAnimation { + duration: 500 + } + ParallelAnimation { + PropertyAnimation { + target: cursor + property: "opacity" + to: 0 + duration: General.animationDuration / 5 + } + PropertyAnimation { + target: cursor + property: "scale" + to: 0 + duration: General.animationDuration / 5 + } + } + PauseAnimation { + duration: 500 + } + ParallelAnimation { + PropertyAnimation { + target: cursor + property: "opacity" + to: 1 + duration: General.animationDuration / 5 + } + PropertyAnimation { + target: cursor + property: "scale" + to: 1 + duration: General.animationDuration / 5 + } + } + } + } + HoverHandler { + cursorShape: Qt.IBeamCursor + } + onTextChanged: { + if (launcher.state.input !== textInput.text) + pendingUpdate = true; + + Qt.callLater(() => { + if (pendingUpdate) { + pendingUpdate = false; + launcher.state.input = textInput.text; // TODO: find ways to optimize this, like how i was using an alias + } + }); + } + onCursorPositionChanged: { + launcher.state.cursorPosition = textInput.cursorPosition; + } + // onActiveFocusChanged: { + // parentLoader.active = activeFocus; + // } + onAccepted: { + launcher.state?.priorities[launcher.state.selectedPriority]?._exec(); // TODO: kde like waiting animation for app to launch + parentLoader.active = false; + textInput.text = ""; + } + Component.onCompleted: { + selectAll(); + } + } + IRect { + opacity: launcher.state?.priorities[launcher.state.selectedPriority]?.predictiveCompletion ? 1 : 0 + Layout.fillHeight: true + Layout.preferredWidth: text.width + radius: Launcher.predictiveCompletionRadius + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + IText { + id: text + // animate: true // too jumpy + color: Colors.foreground3 + renderType: Text.CurveRendering + antialiasing: true + smooth: true + font { + pixelSize: Style.font.size.large + family: Style.font.family.sans + wordSpacing: 5 + } + text: launcher.state?.priorities[launcher.state.selectedPriority]?.predictiveCompletion || "" + } + Behavior on Layout.preferredWidth { + ISpringAnimation {} + // NumberAnimation { + // duration: General.animationDuration / 5 + // } + } + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 2 + easing.type: Easing.InOutQuad + } + } + } + Item { + Layout.fillWidth: true + } + Loader { + active: launcher.state?.priorities[launcher.state.selectedPriority]?.priority || false // .priority should always be true if it's in priorities + sourceComponent: launcher.state?.priorities[launcher.state.selectedPriority]?.preview + } + } + } + + ColumnLayout { + id: widgets + anchors { + top: searchBar.bottom + left: parent.left + right: parent.right + topMargin: Launcher.innerMargin * 1.5 // gap in between, maybe seperator FIXME + } + + Repeater { + model: launcher.widgetItems + delegate: Loader { + active: true + visible: item?.valid ?? true // not item?.standalone cuz when its standalone it doesnt appear in launcher.widgetItems + sourceComponent: modelData + asynchronous: true + onLoaded: { + launcher.state.widgets[index] = item; + // FIXME: prev predictiveCompletion still exists here + // workaround atm: just clear it or preserve the text manually + } + + Connections { + target: item + ignoreUnknownSignals: true + + function onClose() { + parentLoader.active = false; + } + } + } + } + } + } + + // not Keys.onPressed because textinput will steal it + Shortcut { + sequence: "Escape" + context: Qt.ApplicationShortcut + onActivated: parentLoader.active = false + } + + Shortcut { + sequence: "Ctrl+H" + context: Qt.ApplicationShortcut + onActivated: changePos(1) + } + + Shortcut { + sequence: "Ctrl+J" + context: Qt.ApplicationShortcut + onActivated: changePos(-2) + } + + Shortcut { + sequence: "Ctrl+K" + context: Qt.ApplicationShortcut + onActivated: changePos(2) + } + + Shortcut { + sequence: "Ctrl+L" + context: Qt.ApplicationShortcut + onActivated: changePos(-1) + } + + Shortcut { + sequence: "Down" + context: Qt.ApplicationShortcut + onActivated: changePos(-2) + } + + Shortcut { + sequence: "Up" + context: Qt.ApplicationShortcut + onActivated: changePos(2) + } + + Shortcut { + sequence: "PgDown" + context: Qt.ApplicationShortcut + onActivated: changePos(-20) + } + + Shortcut { + sequence: "PgUp" + context: Qt.ApplicationShortcut + onActivated: changePos(20) + } + + Shortcut { + id: homeShortcut + sequence: "Home" + context: Qt.ApplicationShortcut + onActivated: changePos(200) + } + + Shortcut { + id: endShortcut + sequence: StandardKey.MoveToEndOfDocument + context: Qt.ApplicationShortcut + onActivated: changePos(-200) + } + } + // 1 = left + // -1 = right + // 2 = up + // -2 = down + function changePos(direction: int) { + const item = launcher.state?.priorities[launcher.state.selectedPriority]; + if (!item) + return; + + let set = 0; + + switch (direction) { + case 1: + if (item.prev()) + set = -1; + break; + case -1: + if (item.next()) + set = 1; + break; + case 2: + if (item.up()) + set = -1; + break; + case -2: + if (item.down()) + set = 1; + break; + case 20: + if (item.pgup()) + set = -1; + break; + case -20: + if (item.pgdn()) + set = 1; + break; + case 200: + if (item.home()) + set = -1; + break; + case -200: + if (item.end()) + set = 1; + break; + } + + if (set !== 0) { + switch (set) { + case 1: + if (launcher.state.selectedPriority < launcher.state.priorities.length - 1) { + launcher.state.selectedPriority++; + launcher.state.priorities[launcher.state.selectedPriority].home(); + } + break; + case -1: + if (launcher.state.selectedPriority > 0) { + launcher.state.selectedPriority--; + launcher.state.priorities[launcher.state.selectedPriority].end(); + } + break; + } + } + } + + implicitWidth: container.width + implicitHeight: container.height + + Connections { + target: launcher?.state + // launcher.state may be null + ignoreUnknownSignals: true + + function onSyncSelectionState() { + selectionSync.running = true; + } + } } diff --git a/popups/launcher/components/App.qml b/popups/launcher/components/App.qml index aa278f1..1fa27a7 100644 --- a/popups/launcher/components/App.qml +++ b/popups/launcher/components/App.qml @@ -8,183 +8,184 @@ import qs.services import qs.config IComponent { - id: root - - property list<DesktopEntry> entries: AppSearch.query(inputCleaned) - property int selectedIndex: -1 - - name: "Applications" - - implicitHeight: valid ? layout.height : 0 - - preview: Component { - IconImage { - source: Quickshell.iconPath(answer, "image-missing") - implicitWidth: General.appIconSize - implicitHeight: General.appIconSize - } - } - - property string predictiveCompletion: entries[selectedIndex]?.name.slice(input.length) || "" - - process: function () { - const valid = entries.length > 0; - // TODO: consider removing the valid check - const selected = valid ? entries[selectedIndex] : ""; - return { - valid, - priority: valid, - answer: selected?.icon || "" - }; - } - - exec: function () { - SelectionState.selected?.modelData?.execute(); - } - - up: function () { - if (selectedIndex <= 0) - return true; - selectedIndex--; - } - down: function () { - if (selectedIndex + 1 > listView.count - 1) - return true; - selectedIndex++; - } - - home: function () { - if (selectedIndex <= 0) - return true; - selectedIndex = 0; - } - end: function () { - if (selectedIndex + 1 > listView.count - 1) - return true; - selectedIndex = listView.count - 1; - } - - pgup: function () { - if (selectedIndex <= 0) - return true; - - const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); - - if (selectedIndex - pageSize < 0) - selectedIndex = 0; - else - selectedIndex -= pageSize; - } - - pgdn: function () { - if (selectedIndex === entries.length - 1) - return true; - - const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); - if (selectedIndex + pageSize >= listView.count) { - selectedIndex = listView.count - 1; - } else { - selectedIndex += pageSize; - } - } - - onEntriesChanged: { - if (selectedIndex !== -1) { - if (selectedIndex > entries.length - 1) - selectedIndex = entries.length - 1; - - syncSelectionState(); - return; - } - - selectedIndex = 0; - syncSelectionState(); - } - - onSelectedIndexChanged: { - Qt.callLater(() => { - // so at least it exist - listView.positionViewAtIndex(selectedIndex, ListView.Visible); - syncSelectionState(); - listView.positionViewAtIndex(selectedIndex, ListView.Contain); - }); - } - - syncSelectionState: function () { - Qt.callLater(() => { - if (selectedIndex < 0 || selectedIndex >= listView.count || !root.visible) - return; - - state.selected = listView.itemAtIndex(selectedIndex); - }); - } - - IInnerComponent { - id: layout - fromParent: false - width: parent.width - height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) - - ListView { - id: listView - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: entries - spacing: 0 - - delegate: Item { - required property DesktopEntry modelData - required property int index - visible: modelData !== null - width: item.implicitWidth + Launcher.innerMargin * 4 - height: item.height + Launcher.innerMargin * 4 - - RowLayout { - id: item - scale: root.isSelectedPriority() && index === selectedIndex ? 1.01 : 0.99 - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: Launcher.innerMargin * 2 - spacing: Launcher.innerMargin * 2 - - IconImage { - source: modelData ? Quickshell.iconPath(modelData.icon, "image-missing") : "" - implicitWidth: General.appIconSize - implicitHeight: General.appIconSize - } - IText { - text: modelData ? modelData.name : "" - renderType: Text.QtRendering - font.bold: root.isSelectedPriority() && index === selectedIndex - color: root.isSelectedPriority() && index === selectedIndex ? Colors.foreground1 : Colors.foreground2 - Behavior on color { - ColorAnimation { - duration: General.animationDuration - easing.type: Easing.InOutQuad - } - } - } - - Behavior on scale { - ISpringAnimation {} - } - } - - IMouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered_: { - root.selectedIndex = index; - } - onPressed: { - root._exec(); - } - } - } - onContentYChanged: { - SelectionState.syncSelectionState(); - } - } - } + id: root + + property list<DesktopEntry> entries: AppSearch.query(inputCleaned) + property int selectedIndex: -1 + + name: "Applications" + + implicitHeight: valid ? layout.height : 0 + + preview: Component { + IconImage { + source: Quickshell.iconPath(answer, "image-missing") + implicitWidth: General.appIconSize + implicitHeight: General.appIconSize + } + } + + property string predictiveCompletion: entries[selectedIndex]?.name.slice(input.length) || "" + + process: function () { + const valid = entries.length > 0; + // TODO: consider removing the valid check + const selected = valid ? entries[selectedIndex] : ""; + return { + valid, + priority: valid, + answer: selected?.icon || "" + }; + } + + exec: function () { + SelectionState.selected?.modelData?.execute(); + } + + up: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + down: function () { + if (selectedIndex + 1 > listView.count - 1) + return true; + selectedIndex++; + } + + home: function () { + if (selectedIndex <= 0) + return true; + selectedIndex = 0; + } + end: function () { + if (selectedIndex + 1 > listView.count - 1) + return true; + selectedIndex = listView.count - 1; + } + + pgup: function () { + if (selectedIndex <= 0) + return true; + + const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); + + if (selectedIndex - pageSize < 0) + selectedIndex = 0; + else + selectedIndex -= pageSize; + } + + pgdn: function () { + if (selectedIndex === entries.length - 1) + return true; + + const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); + if (selectedIndex + pageSize >= listView.count) { + selectedIndex = listView.count - 1; + } else { + selectedIndex += pageSize; + } + } + + onEntriesChanged: { + if (selectedIndex !== -1) { + if (selectedIndex > entries.length - 1) + selectedIndex = entries.length - 1; + + syncSelectionState(); + return; + } + + selectedIndex = 0; + syncSelectionState(); + } + + onSelectedIndexChanged: { + Qt.callLater(() => { + // so at least it exist + listView.scrollToIndex(selectedIndex, ListView.Visible); + syncSelectionState(); + listView.scrollToIndex(selectedIndex, ListView.Contain); + }); + } + + syncSelectionState: function () { + Qt.callLater(() => { + if (selectedIndex < 0 || selectedIndex >= listView.count || !root.visible) + return; + + state.selected = listView.itemAtIndex(selectedIndex); + }); + } + + IInnerComponent { + id: layout + fromParent: false + width: parent.width + height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) + + IListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: entries + spacing: 0 + + highlightMoveDuration: 500 + delegate: Item { + required property DesktopEntry modelData + required property int index + visible: modelData !== null + width: item.implicitWidth + Launcher.innerMargin * 4 + height: item.height + Launcher.innerMargin * 4 + + RowLayout { + id: item + scale: root.isSelectedPriority() && index === selectedIndex ? 1.01 : 0.99 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Launcher.innerMargin * 2 + spacing: Launcher.innerMargin * 2 + + IconImage { + source: modelData ? Quickshell.iconPath(modelData.icon, "image-missing") : "" + implicitWidth: General.appIconSize + implicitHeight: General.appIconSize + } + IText { + text: modelData ? modelData.name : "" + renderType: Text.QtRendering + font.bold: root.isSelectedPriority() && index === selectedIndex + color: root.isSelectedPriority() && index === selectedIndex ? Colors.foreground1 : Colors.foreground2 + Behavior on color { + ColorAnimation { + duration: General.animationDuration + easing.type: Easing.InOutQuad + } + } + } + + Behavior on scale { + ISpringAnimation {} + } + } + + IMouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered_: { + root.selectedIndex = index; + } + onPressed: { + root._exec(); + } + } + } + onContentYChanged: { + SelectionState.syncSelectionState(); + } + } + } } diff --git a/popups/launcher/components/Calc.qml b/popups/launcher/components/Calc.qml index 4daae66..d3b5043 100644 --- a/popups/launcher/components/Calc.qml +++ b/popups/launcher/components/Calc.qml @@ -9,129 +9,129 @@ import qs.config import core IComponent { - id: root - - property int cursorPosition: SelectionState.cursorPosition - - name: "Calculator" - preview: Component { - Icon { - text: "" - } - } - - onInputChanged: { - MathCalc.reset_result(); - } - - process: function () { - MathCalc.query(input); - const valid = MathCalc.result && MathCalc.result.length > 0; - const answer = MathCalc.result ? MathCalc.result - // 1. Calculus & Summations (Case insensitive) - .replace(/\bint(?:egral)?\b/gi, "∫").replace(/\bsum\b/gi, "∑") - - // 2. Constants & Symbols (Case insensitive) - .replace(/\bpi\b/gi, "π").replace(/\btau\b/gi, "τ").replace(/\binf(?:inity)?\b/gi, "∞").replace(/\bdeg\b/gi, "°") - - // 3. Roots & Exponentials (Case insensitive) - .replace(/\bsqrt\b/gi, "√").replace(/\bcbrt\b/gi, "∛") - - // 4. Rounding & Absolute Value (Case insensitive) - .replace(/\bceil\(([^\)]+)\)/gi, "⌈$1⌉").replace(/\bfloor\(([^\)]+)\)/gi, "⌊$1⌋").replace(/\bround\b/gi, " ≈ ") - - // 5. Comparison & Equality - .replace(/\s*<=\s*/g, " ≤ ").replace(/\s*>=\s*/g, " ≥ ").replace(/\s*!=\s*/g, " ≠ ").replace(/\s*==\s*/g, " = ") - - // 6. Exponents to Unicode Superscripts FIRST (Case insensitive for ^n, ^x, ^y) - .replace(/\^0/g, "⁰").replace(/\^1/g, "¹").replace(/\^2/g, "²").replace(/\^3/g, "³").replace(/\^4/g, "⁴").replace(/\^5/g, "⁵").replace(/\^6/g, "⁶").replace(/\^7/g, "⁷").replace(/\^8/g, "⁸").replace(/\^9/g, "⁹").replace(/\^\+/g, "⁺").replace(/\^-/g, "⁻").replace(/\^n/gi, "ⁿ").replace(/\^x/gi, "ˣ").replace(/\^y/gi, "ʸ") - - // 7. Kalk Number Bases - .replace(/_0/g, "₀").replace(/_10/g, "₁₀").replace(/_16/g, "₁₆").replace(/_1/g, "₁").replace(/_2/g, "₂").replace(/_3/g, "₃").replace(/_4/g, "₄").replace(/_5/g, "₅").replace(/_6/g, "₆").replace(/_7/g, "₇").replace(/_8/g, "₈").replace(/_9/g, "₉") - - // 8. Arithmetic Operators - .replace(/\s*\*\s*/g, " × ").replace(/\s*\/\s*/g, " ÷ ").replace(/\s*-\s*/g, " − ").replace(/\s*\+\s*/g, " + ") : ""; - return { - valid, - priority: valid, - answer, - predictiveCompletion: valid ? ' = ' + answer : '' - }; - } - exec: function () { - Clip.copy(answer); - MathCalc.reset_ctx(); - } - - IInnerComponent { - RowLayout { - spacing: 0 - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: parent.height - - Item { - Layout.fillWidth: true - IText { - anchors.centerIn: parent - elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight - width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) - clip: true - renderType: Text.CurveRendering - visible: valid - text: input - // 1. Calculus & Summations (Case insensitive) - .replace(/\bint(?:egral)?\b/gi, "∫").replace(/\bsum\b/gi, "∑") - - // 2. Constants & Symbols (Case insensitive) - .replace(/\bpi\b/gi, "π").replace(/\btau\b/gi, "τ").replace(/\binf(?:inity)?\b/gi, "∞").replace(/\bdeg\b/gi, "°") - - // 3. Roots & Exponentials (Case insensitive) - .replace(/\bsqrt\b/gi, "√").replace(/\bcbrt\b/gi, "∛") - - // 4. Rounding & Absolute Value (Case insensitive) - .replace(/\bceil\(([^\)]+)\)/gi, "⌈$1⌉").replace(/\bfloor\(([^\)]+)\)/gi, "⌊$1⌋").replace(/\bround\b/gi, " ≈ ") - - // 5. Comparison & Equality - .replace(/\s*<=\s*/g, " ≤ ").replace(/\s*>=\s*/g, " ≥ ").replace(/\s*!=\s*/g, " ≠ ").replace(/\s*==\s*/g, " = ") - - // 6. Exponents to Unicode Superscripts FIRST (Case insensitive for ^n, ^x, ^y) - .replace(/\^0/g, "⁰").replace(/\^1/g, "¹").replace(/\^2/g, "²").replace(/\^3/g, "³").replace(/\^4/g, "⁴").replace(/\^5/g, "⁵").replace(/\^6/g, "⁶").replace(/\^7/g, "⁷").replace(/\^8/g, "⁸").replace(/\^9/g, "⁹").replace(/\^\+/g, "⁺").replace(/\^-/g, "⁻").replace(/\^n/gi, "ⁿ").replace(/\^x/gi, "ˣ").replace(/\^y/gi, "ʸ") - - // 7. Kalk Number Bases - .replace(/_0/g, "₀").replace(/_10/g, "₁₀").replace(/_16/g, "₁₆").replace(/_1/g, "₁").replace(/_2/g, "₂").replace(/_3/g, "₃").replace(/_4/g, "₄").replace(/_5/g, "₅").replace(/_6/g, "₆").replace(/_7/g, "₇").replace(/_8/g, "₈").replace(/_9/g, "₉") - - // 8. Arithmetic Operators - .replace(/\s*\*\s*/g, " × ").replace(/\s*\/\s*/g, " ÷ ").replace(/\s*-\s*/g, " − ").replace(/\s*\+\s*/g, " + ") - font { - pixelSize: Launcher.widgetFontSize - bold: root.isSelectedPriority() - } - } - } - - IText { - visible: valid - text: "→" - font.pixelSize: Launcher.widgetFontSize - font.bold: root.isSelectedPriority() - } - - Item { - Layout.fillWidth: true - IText { - animate: true - anchors.centerIn: parent - width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) - renderType: Text.CurveRendering - visible: valid - text: valid ? answer : '' - font { - pixelSize: Launcher.widgetFontSize - bold: root.isSelectedPriority() - } - } - } - } - } + id: root + + property int cursorPosition: SelectionState.cursorPosition + + name: "Calculator" + preview: Component { + Icon { + text: "" + } + } + + onInputChanged: { + MathCalc.reset_result(); + } + + process: function () { + MathCalc.query(input); + const valid = MathCalc.result && MathCalc.result.length > 0; + const answer = MathCalc.result ? MathCalc.result + // 1. Calculus & Summations (Case insensitive) + .replace(/\bint(?:egral)?\b/gi, "∫").replace(/\bsum\b/gi, "∑") + + // 2. Constants & Symbols (Case insensitive) + .replace(/\bpi\b/gi, "π").replace(/\btau\b/gi, "τ").replace(/\binf(?:inity)?\b/gi, "∞").replace(/\bdeg\b/gi, "°") + + // 3. Roots & Exponentials (Case insensitive) + .replace(/\bsqrt\b/gi, "√").replace(/\bcbrt\b/gi, "∛") + + // 4. Rounding & Absolute Value (Case insensitive) + .replace(/\bceil\(([^\)]+)\)/gi, "⌈$1⌉").replace(/\bfloor\(([^\)]+)\)/gi, "⌊$1⌋").replace(/\bround\b/gi, " ≈ ") + + // 5. Comparison & Equality + .replace(/\s*<=\s*/g, " ≤ ").replace(/\s*>=\s*/g, " ≥ ").replace(/\s*!=\s*/g, " ≠ ").replace(/\s*==\s*/g, " = ") + + // 6. Exponents to Unicode Superscripts FIRST (Case insensitive for ^n, ^x, ^y) + .replace(/\^0/g, "⁰").replace(/\^1/g, "¹").replace(/\^2/g, "²").replace(/\^3/g, "³").replace(/\^4/g, "⁴").replace(/\^5/g, "⁵").replace(/\^6/g, "⁶").replace(/\^7/g, "⁷").replace(/\^8/g, "⁸").replace(/\^9/g, "⁹").replace(/\^\+/g, "⁺").replace(/\^-/g, "⁻").replace(/\^n/gi, "ⁿ").replace(/\^x/gi, "ˣ").replace(/\^y/gi, "ʸ") + + // 7. Kalk Number Bases + .replace(/_0/g, "₀").replace(/_10/g, "₁₀").replace(/_16/g, "₁₆").replace(/_1/g, "₁").replace(/_2/g, "₂").replace(/_3/g, "₃").replace(/_4/g, "₄").replace(/_5/g, "₅").replace(/_6/g, "₆").replace(/_7/g, "₇").replace(/_8/g, "₈").replace(/_9/g, "₉") + + // 8. Arithmetic Operators + .replace(/\s*\*\s*/g, " × ").replace(/\s*\/\s*/g, " ÷ ").replace(/\s*-\s*/g, " − ").replace(/\s*\+\s*/g, " + ") : ""; + return { + valid, + priority: valid, + answer, + predictiveCompletion: valid ? ' = ' + answer : '' + }; + } + exec: function () { + Clip.copy(answer); + MathCalc.reset_ctx(); + } + + IInnerComponent { + RowLayout { + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + + Item { + Layout.fillWidth: true + IText { + anchors.centerIn: parent + elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + clip: true + renderType: Text.CurveRendering + visible: valid + text: input + // 1. Calculus & Summations (Case insensitive) + .replace(/\bint(?:egral)?\b/gi, "∫").replace(/\bsum\b/gi, "∑") + + // 2. Constants & Symbols (Case insensitive) + .replace(/\bpi\b/gi, "π").replace(/\btau\b/gi, "τ").replace(/\binf(?:inity)?\b/gi, "∞").replace(/\bdeg\b/gi, "°") + + // 3. Roots & Exponentials (Case insensitive) + .replace(/\bsqrt\b/gi, "√").replace(/\bcbrt\b/gi, "∛") + + // 4. Rounding & Absolute Value (Case insensitive) + .replace(/\bceil\(([^\)]+)\)/gi, "⌈$1⌉").replace(/\bfloor\(([^\)]+)\)/gi, "⌊$1⌋").replace(/\bround\b/gi, " ≈ ") + + // 5. Comparison & Equality + .replace(/\s*<=\s*/g, " ≤ ").replace(/\s*>=\s*/g, " ≥ ").replace(/\s*!=\s*/g, " ≠ ").replace(/\s*==\s*/g, " = ") + + // 6. Exponents to Unicode Superscripts FIRST (Case insensitive for ^n, ^x, ^y) + .replace(/\^0/g, "⁰").replace(/\^1/g, "¹").replace(/\^2/g, "²").replace(/\^3/g, "³").replace(/\^4/g, "⁴").replace(/\^5/g, "⁵").replace(/\^6/g, "⁶").replace(/\^7/g, "⁷").replace(/\^8/g, "⁸").replace(/\^9/g, "⁹").replace(/\^\+/g, "⁺").replace(/\^-/g, "⁻").replace(/\^n/gi, "ⁿ").replace(/\^x/gi, "ˣ").replace(/\^y/gi, "ʸ") + + // 7. Kalk Number Bases + .replace(/_0/g, "₀").replace(/_10/g, "₁₀").replace(/_16/g, "₁₆").replace(/_1/g, "₁").replace(/_2/g, "₂").replace(/_3/g, "₃").replace(/_4/g, "₄").replace(/_5/g, "₅").replace(/_6/g, "₆").replace(/_7/g, "₇").replace(/_8/g, "₈").replace(/_9/g, "₉") + + // 8. Arithmetic Operators + .replace(/\s*\*\s*/g, " × ").replace(/\s*\/\s*/g, " ÷ ").replace(/\s*-\s*/g, " − ").replace(/\s*\+\s*/g, " + ") + font { + pixelSize: Launcher.widgetFontSize + bold: root.isSelectedPriority() + } + } + } + + IText { + visible: valid + text: "→" + font.pixelSize: Launcher.widgetFontSize + font.bold: root.isSelectedPriority() + } + + Item { + Layout.fillWidth: true + IText { + animate: true + anchors.centerIn: parent + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + renderType: Text.CurveRendering + visible: valid + text: valid ? answer : '' + font { + pixelSize: Launcher.widgetFontSize + bold: root.isSelectedPriority() + } + } + } + } + } } diff --git a/popups/launcher/components/Clip.qml b/popups/launcher/components/Clip.qml index 9b7a0a9..a830483 100644 --- a/popups/launcher/components/Clip.qml +++ b/popups/launcher/components/Clip.qml @@ -8,214 +8,214 @@ import qs.services import qs.config IComponent { - id: root - - property var clipHist: active ? Clip.query(inputCleaned) : [] - property int selectedIndex: -1 - - name: "Clipboard" - - prefix: "clip" - - implicitHeight: valid ? layout.height : 0 - - process: function () { - const isValid = clipHist.length > 0; - return { - valid: isValid, - priority: isValid - }; - } - - exec: function () { - Clip.decodeAndCopy(clipHist[selectedIndex].raw); - } - - up: function () { - if (selectedIndex <= 0) - return true; - selectedIndex--; - } - down: function () { - if (selectedIndex + 1 > listView.count - 1) - return true; - selectedIndex++; - } - home: function () { - if (selectedIndex <= 0) - return true; - selectedIndex = 0; - } - end: function () { - if (selectedIndex + 1 > listView.count - 1) - return true; - selectedIndex = listView.count - 1; - } - // TODO: impl pgup pgdn - - onClipHistChanged: { - if (selectedIndex !== -1) { - syncSelectionState(); - return; - } - - selectedIndex = 0; - syncSelectionState(); - } - - onSelectedIndexChanged: { - if (selectedIndex >= 0 && selectedIndex < listView.count) - Qt.callLater(() => { - listView.positionViewAtIndex(selectedIndex, ListView.Visible); - syncSelectionState(); - listView.positionViewAtIndex(selectedIndex, ListView.Contain); - }); - } - - syncSelectionState: function () { - Qt.callLater(() => { - if (!root.visible || selectedIndex < 0 || selectedIndex >= listView.count) - return; - - trySetSelfPriority(); - state.selected = listView.itemAtIndex(selectedIndex); - }); - } - - IInnerComponent { - id: layout - fromParent: false - width: parent.width - height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - ListView { - id: listView - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: clipHist - spacing: 0 - - delegate: Item { - required property var modelData - required property int index - width: Math.min(item.implicitWidth + Launcher.innerMargin * 4, listView.width) - height: item.height + Launcher.innerMargin * 4 - - Row { - id: item - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: Launcher.innerMargin * 2 - spacing: Launcher.innerMargin * 2 - clip: true - - IconImage { - scale: root.isSelectedPriority() && index === root.selectedIndex ? 1.01 : 0.9 - source: modelData?.appIcon - implicitSize: parent.height - - Behavior on scale { - ISpringAnimation {} - } - } - - Column { - Loader { - sourceComponent: modelData?.type === "image" ? img : text - readonly property Component text: IText { - text: String(modelData?.data || "").substring(0, General.maxClipPreviewChar).replace(/\n/g, " ") - color: root.isSelectedPriority() && index === selectedIndex ? Colors.foreground1 : Colors.foreground2 - font.pixelSize: Launcher.widgetFontSize - font.bold: root.isSelectedPriority() && index === selectedIndex - } - readonly property Component img: Image { - source: modelData?.data || "" - height: Launcher.widgetFontSize * 1.3 - fillMode: Image.PreserveAspectFit - scale: root.isSelectedPriority() && index === root.selectedIndex ? 1.01 : 0.9 - - MouseArea { - anchors.fill: parent - onClicked: { - Quickshell.execDetached(["xdg-open", modelData?.data]); - } - } - - Behavior on scale { - ISpringAnimation {} - } - } - } - Row { - spacing: Launcher.innerMargin - property string sinceWhen: TimeDate.sinceWhen(modelData?.timestamp) || "" - IText { - text: modelData.type - color: isSelectedPriority() && index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 - font.pixelSize: Launcher.widgetFontSize / 1.35 - } - IText { - visible: parent.sinceWhen - text: '·' - color: isSelectedPriority() && index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 - font.pixelSize: Launcher.widgetFontSize / 1.35 - } - IText { - text: parent.sinceWhen || "" - color: root.isSelectedPriority() && index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 - font.pixelSize: Launcher.widgetFontSize / 1.35 - } - } - } - } - } - } - - Flickable { - id: preview - clip: true - Layout.fillWidth: true - Layout.fillHeight: true - - contentWidth: loader.item.width - contentHeight: loader.item.height - flickableDirection: Flickable.VerticalFlick - boundsBehavior: Flickable.StopAtBounds - - Loader { - id: loader - asynchronous: true - sourceComponent: clipHist[selectedIndex]?.type === "image" ? img : text - property Component text: ITextEdit { - id: textEdit - animate: true - readOnly: true - wrapMode: TextEdit.Wrap - width: preview.width - color: Colors.foreground1 - selectionColor: Colors.background3 - text: clipHist[selectedIndex]?.decoded || "" - } - property Component img: Image { - id: image - asynchronous: true - width: preview.width - height: preview.height - source: clipHist[selectedIndex]?.image || "" - fillMode: Image.PreserveAspectFit - TapHandler { - onTapped: { - Quickshell.execDetached(["xdg-open", clipHist[selectedIndex]?.image]); - } - } - } - } - } - } - } + id: root + + property var clipHist: active ? Clip.query(inputCleaned) : [] + property int selectedIndex: -1 + + name: "Clipboard" + + prefix: "clip" + + implicitHeight: valid ? layout.height : 0 + + process: function () { + const isValid = clipHist.length > 0; + return { + valid: isValid, + priority: isValid + }; + } + + exec: function () { + Clip.decodeAndCopy(clipHist[selectedIndex].raw); + } + + up: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + down: function () { + if (selectedIndex + 1 > listView.count - 1) + return true; + selectedIndex++; + } + home: function () { + if (selectedIndex <= 0) + return true; + selectedIndex = 0; + } + end: function () { + if (selectedIndex + 1 > listView.count - 1) + return true; + selectedIndex = listView.count - 1; + } + // TODO: impl pgup pgdn + + onClipHistChanged: { + if (selectedIndex !== -1) { + syncSelectionState(); + return; + } + + selectedIndex = 0; + syncSelectionState(); + } + + onSelectedIndexChanged: { + if (selectedIndex >= 0 && selectedIndex < listView.count) + Qt.callLater(() => { + listView.scrollToIndex(selectedIndex, ListView.Visible); + syncSelectionState(); + listView.scrollToIndex(selectedIndex, ListView.Contain); + }); + } + + syncSelectionState: function () { + Qt.callLater(() => { + if (!root.visible || selectedIndex < 0 || selectedIndex >= listView.count) + return; + + trySetSelfPriority(); + state.selected = listView.itemAtIndex(selectedIndex); + }); + } + + IInnerComponent { + id: layout + fromParent: false + width: parent.width + height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + IListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: clipHist + spacing: 0 + + delegate: Item { + required property var modelData + required property int index + width: Math.min(item.implicitWidth + Launcher.innerMargin * 4, listView.width) + height: item.height + Launcher.innerMargin * 4 + + Row { + id: item + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Launcher.innerMargin * 2 + spacing: Launcher.innerMargin * 2 + clip: true + + IconImage { + scale: root.isSelectedPriority() && index === root.selectedIndex ? 1.01 : 0.9 + source: modelData?.appIcon + implicitSize: parent.height + + Behavior on scale { + ISpringAnimation {} + } + } + + Column { + Loader { + sourceComponent: modelData?.type === "image" ? img : text + readonly property Component text: IText { + text: String(modelData?.data || "").substring(0, General.maxClipPreviewChar).replace(/\n/g, " ") + color: root.isSelectedPriority() && index === selectedIndex ? Colors.foreground1 : Colors.foreground2 + font.pixelSize: Launcher.widgetFontSize + font.bold: root.isSelectedPriority() && index === selectedIndex + } + readonly property Component img: Image { + source: modelData?.data || "" + height: Launcher.widgetFontSize * 1.3 + fillMode: Image.PreserveAspectFit + scale: root.isSelectedPriority() && index === root.selectedIndex ? 1.01 : 0.9 + + MouseArea { + anchors.fill: parent + onClicked: { + Quickshell.execDetached(["xdg-open", modelData?.data]); + } + } + + Behavior on scale { + ISpringAnimation {} + } + } + } + Row { + spacing: Launcher.innerMargin + property string sinceWhen: TimeDate.sinceWhen(modelData?.timestamp) || "" + IText { + text: modelData.type + color: isSelectedPriority() && index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 + font.pixelSize: Launcher.widgetFontSize / 1.35 + } + IText { + visible: parent.sinceWhen + text: '·' + color: isSelectedPriority() && index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 + font.pixelSize: Launcher.widgetFontSize / 1.35 + } + IText { + text: parent.sinceWhen || "" + color: root.isSelectedPriority() && index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 + font.pixelSize: Launcher.widgetFontSize / 1.35 + } + } + } + } + } + } + + Flickable { + id: preview + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + + contentWidth: loader.item.width + contentHeight: loader.item.height + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + + Loader { + id: loader + asynchronous: true + sourceComponent: clipHist[selectedIndex]?.type === "image" ? img : text + property Component text: ITextEdit { + id: textEdit + animate: true + readOnly: true + wrapMode: TextEdit.Wrap + width: preview.width + color: Colors.foreground1 + selectionColor: Colors.background3 + text: clipHist[selectedIndex]?.decoded || "" + } + property Component img: Image { + id: image + asynchronous: true + width: preview.width + height: preview.height + source: clipHist[selectedIndex]?.image || "" + fillMode: Image.PreserveAspectFit + TapHandler { + onTapped: { + Quickshell.execDetached(["xdg-open", clipHist[selectedIndex]?.image]); + } + } + } + } + } + } + } } diff --git a/popups/launcher/components/DuckDuckGo.qml b/popups/launcher/components/DuckDuckGo.qml index 379f56b..ee5b752 100644 --- a/popups/launcher/components/DuckDuckGo.qml +++ b/popups/launcher/components/DuckDuckGo.qml @@ -10,154 +10,154 @@ import qs.config import core IComponent { - id: root - - property int cursorPosition: SelectionState.cursorPosition - property bool expand: false - - implicitHeight: valid ? expand ? layout.implicitHeight + (Launcher.duckduckgoSpacing * 2) : Launcher.widgetHeight : 0 - - name: "DuckDuckGo" // subclass of WebSearch in the future - preview: Component { - Icon { - text: "🌐" - } - } - - onInputChanged: { - // TODO: think of this should be part of rs backend .query fn - DuckDuckGo.reset(); - } - - // TODO: decouple all query logic from process -> onInputCleanedChanged and resetting / temp state on speciifc input -> onInputChanged - onInputCleanedChanged: { - if (inputCleaned) - DuckDuckGo.query(inputCleaned); - } - - process: function () { - const valid = DuckDuckGo.ok; - const answer = DuckDuckGo.title || DuckDuckGo.description_html; - return { - valid, - priority: valid, - answer, - predictiveCompletion: valid && inputCleaned.toLowerCase() != answer.toLowerCase().trim() ? ' is ' + answer : '' - }; - } - exec: function () { - // FIXME: doesn't work as enter closes the launcher - // if (!expand) { - // expand = true; - // return false; - // } else - Clip.copy(`${DuckDuckGo.title}\n${DuckDuckGo.description_html}`); - } - - IInnerComponent { - anchors.margins: Launcher.duckduckgoSpacing - RowLayout { - id: layout - spacing: Launcher.duckduckgoSpacing - Layout.fillWidth: true - Layout.fillHeight: true - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: 0 - Layout.maximumWidth: image.maxWidth - Layout.minimumWidth: 0 - - visible: image.status === Image.Ready - - Image { - id: image - property real maxWidth: status === Image.Ready ? implicitWidth : 0 - anchors.fill: parent - scale: root.isSelectedPriority() ? 1.01 : 0.99 - - fillMode: Image.PreserveAspectCrop - - opacity: status === Image.Ready ? 1 : 0 - - source: DuckDuckGo.image - asynchronous: true - - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 4 - } - } - - Behavior on maxWidth { - ISpringAnimation {} - } - - Behavior on scale { - ISpringAnimation {} - } - } - } - - ColumnLayout { - Layout.preferredWidth: 3 - Layout.minimumWidth: 0 - Layout.fillHeight: true - Layout.fillWidth: true - - spacing: Launcher.duckduckgoImageSpacing / 4 - - ITextEdit { - animate: true - - Layout.alignment: Qt.AlignTop - Layout.fillWidth: true - - readOnly: true - renderType: Text.CurveRendering - visible: valid - text: answer - font { - pixelSize: Launcher.widgetFontSize * 1.1 - bold: root.isSelectedPriority() - } - } - - ScrollView { - id: scroll - Layout.fillWidth: true - Layout.fillHeight: true - contentWidth: availableWidth - clip: true - - ITextEdit { - id: text - - animate: true - - height: scroll.contentHeight - width: scroll.contentWidth - - readOnly: true - renderType: Text.CurveRendering - visible: valid && DuckDuckGo.title - text: DuckDuckGo.title ? DuckDuckGo.description_html : '' - font { - pixelSize: Launcher.widgetFontSize - } - - TapHandler { - gesturePolicy: TapHandler.DragThreshold - onTapped: expand = !expand - } - } - } - } - } - } - - Behavior on implicitHeight { - ISpringAnimation {} - } + id: root + + property int cursorPosition: SelectionState.cursorPosition + property bool expand: false + + implicitHeight: valid ? expand ? layout.implicitHeight + (Launcher.duckduckgoSpacing * 2) : Launcher.widgetHeight : 0 + + name: "DuckDuckGo" // subclass of WebSearch in the future + preview: Component { + Icon { + text: "🌐" + } + } + + onInputChanged: { + // TODO: think of this should be part of rs backend .query fn + DuckDuckGo.reset(); + } + + // TODO: decouple all query logic from process -> onInputCleanedChanged and resetting / temp state on speciifc input -> onInputChanged + onInputCleanedChanged: { + if (inputCleaned) + DuckDuckGo.query(inputCleaned); + } + + process: function () { + const valid = DuckDuckGo.ok; + const answer = DuckDuckGo.title || DuckDuckGo.description_html; + return { + valid, + priority: valid, + answer, + predictiveCompletion: valid && inputCleaned.toLowerCase() != answer.toLowerCase().trim() ? ' is ' + answer : '' + }; + } + exec: function () { + // FIXME: doesn't work as enter closes the launcher + // if (!expand) { + // expand = true; + // return false; + // } else + Clip.copy(`${DuckDuckGo.title}\n${DuckDuckGo.description_html}`); + } + + IInnerComponent { + anchors.margins: Launcher.duckduckgoSpacing + RowLayout { + id: layout + spacing: Launcher.duckduckgoSpacing + Layout.fillWidth: true + Layout.fillHeight: true + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 0 + Layout.maximumWidth: image.maxWidth + Layout.minimumWidth: 0 + + visible: image.status === Image.Ready + + Image { + id: image + property real maxWidth: status === Image.Ready ? implicitWidth : 0 + anchors.fill: parent + scale: root.isSelectedPriority() ? 1.01 : 0.99 + + fillMode: Image.PreserveAspectCrop + + opacity: status === Image.Ready ? 1 : 0 + + source: DuckDuckGo.image + asynchronous: true + + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 4 + } + } + + Behavior on maxWidth { + ISpringAnimation {} + } + + Behavior on scale { + ISpringAnimation {} + } + } + } + + ColumnLayout { + Layout.preferredWidth: 3 + Layout.minimumWidth: 0 + Layout.fillHeight: true + Layout.fillWidth: true + + spacing: Launcher.duckduckgoImageSpacing / 4 + + ITextEdit { + animate: true + + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + readOnly: true + renderType: Text.CurveRendering + visible: valid + text: answer + font { + pixelSize: Launcher.widgetFontSize * 1.1 + bold: root.isSelectedPriority() + } + } + + ScrollView { + id: scroll + Layout.fillWidth: true + Layout.fillHeight: true + contentWidth: availableWidth + clip: true + + ITextEdit { + id: text + + animate: true + + height: scroll.contentHeight + width: scroll.contentWidth + + readOnly: true + renderType: Text.CurveRendering + visible: valid && DuckDuckGo.title + text: DuckDuckGo.title ? DuckDuckGo.description_html : '' + font { + pixelSize: Launcher.widgetFontSize + } + + TapHandler { + gesturePolicy: TapHandler.DragThreshold + onTapped: expand = !expand + } + } + } + } + } + } + + Behavior on implicitHeight { + ISpringAnimation {} + } } diff --git a/popups/launcher/components/Emoji.qml b/popups/launcher/components/Emoji.qml index ecb1cd4..a74f693 100644 --- a/popups/launcher/components/Emoji.qml +++ b/popups/launcher/components/Emoji.qml @@ -8,181 +8,181 @@ import qs.services import qs.config IComponent { - id: root - - property var emojis: active ? Emoji.query(input) : [] - property int selectedIndex: -1 - - property int cols: Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 4)))) - property int cellWidth: Math.floor(innerLoader.width / cols) - property int cellHeight: Math.ceil(Launcher.widgetFontSize * 2) + Launcher.innerMargin * 2 - - name: "Emoji" - prefix: name.toLowerCase() + " " - - onEmojisChanged: { - Qt.callLater(() => { - if (selectedIndex !== -1) { - syncSelectionState(); - return; - } - - selectedIndex = 0; - if (loader.item && loader.item.flickable) - loader.item.flickable.contentY = 0; - }); - } - - onSelectedIndexChanged: { - Qt.callLater(() => { - if (!loader?.view || selectedIndex < 0 || selectedIndex >= loader.view.count) - return; - - if (!isSelectedPriority() && state.selected === loader.view.itemAtIndex(selectedIndex)) { - state.selected = null; - return; - } - - loader.view.currentIndex = selectedIndex; - loader.view.positionViewAtIndex(selectedIndex, GridView.Contain); - syncSelectionState(); - }); - } - - process: function () { - const isValid = selectedIndex >= 0 && emojis.length > 0; - const select = isValid ? emojis[selectedIndex] : null; - return { - valid: isValid, - priority: isValid, - answer: isValid ? select.emoji : "" - }; - } - - home: function () { - if (!loader.view.count) - return true; - selectedIndex = 0; - } - end: function () { - if (!loader.view.count) - return true; - selectedIndex = loader.view.count - 1; - } - prev: function () { - if (selectedIndex <= 0) - return true; - selectedIndex--; - } - next: function () { - if (selectedIndex + 1 >= emojis.length) - return true; - selectedIndex++; - } - up: function () { - if (selectedIndex - cols < 0) - return true; - selectedIndex -= cols; - } - down: function () { - if (selectedIndex + cols >= emojis.length) - return true; - selectedIndex += cols; - } - - exec: function () { - if (selectedIndex >= 0 && emojis.length > 0) - Emoji.copy(emojis[selectedIndex].emoji); - } - - syncSelectionState: function () { - Qt.callLater(() => { - if (!loader.view || selectedIndex < 0 || selectedIndex >= loader.view.count || !root.visible) - return; - - state.selected = loader.view.itemAtIndex(selectedIndex); - }); - } - - IInnerComponent { - id: innerLoader - - Loader { - id: loader - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: parent.height - sourceComponent: show - property var view: null - - readonly property Component show: Component { - Item { - property alias flickable: grid - - GridView { - id: grid - anchors.fill: parent - clip: true - boundsBehavior: Flickable.StopAtBounds - cacheBuffer: cellHeight * 2 - model: emojis - interactive: true - flow: GridView.FlowLeftToRight - snapMode: GridView.NoSnap - - cellWidth: root.cellWidth - cellHeight: root.cellHeight - - delegate: Item { - required property var modelData - required property int index - width: grid.cellWidth - height: chipContent.implicitHeight + Launcher.innerMargin * 2 - scale: isSelectedPriority() && index === root.selectedIndex ? 1.03 : 0.97 - - IRect { - anchors.fill: parent - color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) - radius: Launcher.widgetRadius - - Column { - id: chipContent - anchors.centerIn: parent - spacing: 2 - - Icon { - anchors.horizontalCenter: parent.horizontalCenter - text: modelData.emoji - font.pixelSize: Launcher.widgetFontSize * 1.2 - } - IText { - anchors.horizontalCenter: parent.horizontalCenter - text: modelData.name - font.pixelSize: Launcher.widgetFontSize * 0.7 - font.bold: isSelectedPriority() && index === selectedIndex - color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 - elide: Text.ElideRight - width: parent.parent.width - Launcher.innerMargin * 2 - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: root.selectedIndex = index - onPressed: root._exec() - } - } - - Behavior on scale { - ISpringAnimation {} - } - } - - Component.onCompleted: loader.view = grid - } - } - } - } - } + id: root + + property var emojis: active ? Emoji.query(input) : [] + property int selectedIndex: -1 + + property int cols: Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 4)))) + property int cellWidth: Math.floor(innerLoader.width / cols) + property int cellHeight: Math.ceil(Launcher.widgetFontSize * 2) + Launcher.innerMargin * 2 + + name: "Emoji" + prefix: name.toLowerCase() + " " + + onEmojisChanged: { + Qt.callLater(() => { + if (selectedIndex !== -1) { + syncSelectionState(); + return; + } + + selectedIndex = 0; + if (loader.item && loader.item.flickable) + loader.item.flickable.contentY = 0; + }); + } + + onSelectedIndexChanged: { + Qt.callLater(() => { + if (!loader?.view || selectedIndex < 0 || selectedIndex >= loader.view.count) + return; + + if (!isSelectedPriority() && state.selected === loader.view.itemAtIndex(selectedIndex)) { + state.selected = null; + return; + } + + loader.view.currentIndex = selectedIndex; + loader.view.scrollToIndex(selectedIndex, GridView.Contain); + syncSelectionState(); + }); + } + + process: function () { + const isValid = selectedIndex >= 0 && emojis.length > 0; + const select = isValid ? emojis[selectedIndex] : null; + return { + valid: isValid, + priority: isValid, + answer: isValid ? select.emoji : "" + }; + } + + home: function () { + if (!loader.view.count) + return true; + selectedIndex = 0; + } + end: function () { + if (!loader.view.count) + return true; + selectedIndex = loader.view.count - 1; + } + prev: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + next: function () { + if (selectedIndex + 1 >= emojis.length) + return true; + selectedIndex++; + } + up: function () { + if (selectedIndex - cols < 0) + return true; + selectedIndex -= cols; + } + down: function () { + if (selectedIndex + cols >= emojis.length) + return true; + selectedIndex += cols; + } + + exec: function () { + if (selectedIndex >= 0 && emojis.length > 0) + Emoji.copy(emojis[selectedIndex].emoji); + } + + syncSelectionState: function () { + Qt.callLater(() => { + if (!loader.view || selectedIndex < 0 || selectedIndex >= loader.view.count || !root.visible) + return; + + state.selected = loader.view.itemAtIndex(selectedIndex); + }); + } + + IInnerComponent { + id: innerLoader + + Loader { + id: loader + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + sourceComponent: show + property var view: null + + readonly property Component show: Component { + Item { + property alias flickable: grid + + GridView { + id: grid + anchors.fill: parent + clip: true + boundsBehavior: Flickable.StopAtBounds + cacheBuffer: cellHeight * 2 + model: emojis + interactive: true + flow: GridView.FlowLeftToRight + snapMode: GridView.NoSnap + + cellWidth: root.cellWidth + cellHeight: root.cellHeight + + delegate: Item { + required property var modelData + required property int index + width: grid.cellWidth + height: chipContent.implicitHeight + Launcher.innerMargin * 2 + scale: isSelectedPriority() && index === root.selectedIndex ? 1.03 : 0.97 + + IRect { + anchors.fill: parent + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + radius: Launcher.widgetRadius + + Column { + id: chipContent + anchors.centerIn: parent + spacing: 2 + + Icon { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.emoji + font.pixelSize: Launcher.widgetFontSize * 1.2 + } + IText { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.name + font.pixelSize: Launcher.widgetFontSize * 0.7 + font.bold: isSelectedPriority() && index === selectedIndex + color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 + elide: Text.ElideRight + width: parent.parent.width - Launcher.innerMargin * 2 + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: root.selectedIndex = index + onPressed: root._exec() + } + } + + Behavior on scale { + ISpringAnimation {} + } + } + + Component.onCompleted: loader.view = grid + } + } + } + } + } } diff --git a/popups/launcher/components/IComponent.qml b/popups/launcher/components/IComponent.qml index e48a0a1..a237f49 100644 --- a/popups/launcher/components/IComponent.qml +++ b/popups/launcher/components/IComponent.qml @@ -5,149 +5,150 @@ import qs.components import qs.config IRect { - id: root - // virtual properties - property bool standalone: false - property var state: SelectionState - property string name // Component/File name - readonly property string _input: state.input - readonly property string input: !standalone ? _input.slice(prefix.length) : _input - readonly property string inputCleaned: input.toLowerCase().trim() - property string prefix - property bool active: !standalone ? input && _input.startsWith(prefix) : true - property bool valid: !standalone ? processed?.valid || false : true - property bool priority: !standalone ? valid && processed?.priority || false : true // please set priority to false if it's invalid - property string answer: processed?.answer || "" - property Component preview: Component { - IText { - // TODO: use proper active: false via Loader - visible: answer.length <= Launcher.maxPreviewLen - animate: true - text: answer - } - } - property string predictiveCompletion: processed?.predictiveCompletion || "" // would technically work with just from answer, but stuff like calc's answer wouldnt have anything to do with the input - property var processed: active ? process() : {} // cached process, thought qml would do that automatically :/ - property var process: () => ({ - valid: valid, - priority: priority, - answer: answer, - preview: preview, - predictiveCompletion: predictiveCompletion - }) - // use the current IRect if valid is set - property var syncSelectionState: () => {} - // actions - property var up: () => ({ - top: true - }) - property var down: () => ({ - bottom: true - }) - property var prev: () => ({ - left: true - }) - property var next: () => ({ - right: true - }) - // true if not closed - property var exec: () => {} - property var home: () => ({ - top: true - }) - property var end: () => ({ - bottom: true - }) - property var pgup: () => ({ - top: true - }) - property var pgdn: () => ({ - bottom: true - }) - - function enter() { - trySetSelfPriority(); - // root or null hmm - state.selected = root; - } - - signal close - - function _exec() { - if (!exec()) - close(); - } - - function getSelfPriority() { - return state.priorities.indexOf(root); - } - - function isSelectedPriority() { - return state.selectedPriority === getSelfPriority(); - } - - function trySetSelfPriority() { - let self = getSelfPriority(); - // ??? why tf is it -1 not null or undefined this is why rust is better - if (self !== -1) - state.selectedPriority = self; - } - - function syncPriority() { - if (!standalone) { - if (state.priorities.includes(root) && !priority) - state.priorities = state.priorities.filter(x => x !== root); - else if (priority) - state.priorities = [...new Set(state.priorities), root]; - } else - state.priorities = [root]; - } - - // this doesn't change launcher size but only hides the actual content but the outside still think the content is there - visible: active - opacity: valid ? 1 : 0 - y: valid ? 0 : -Launcher.widgetHeight - - implicitWidth: valid ? Launcher.widgetWidth : 0 - implicitHeight: valid ? Launcher.widgetHeight : 0 - - radius: Launcher.widgetRadius - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetBgTransparency) // TODO when prioritized, highlight - - onPriorityChanged: { - // sync with state.priorities - Qt.callLater(() => { - syncPriority(); - }); - } - - Behavior on y { - NumberAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - - HoverHandler { - onHoveredChanged: { - if (hovered) { - root.enter(); - root.syncSelectionState(); - } - } - } - - Component.onCompleted: { - Qt.callLater(() => { - syncPriority(); - }); - - // Qt.callLater(() => { - // syncSelectionState(); - // }); - } - - Component.onDestruction: { - state.priorities = state.priorities.filter(x => x !== root); - } + id: root + // virtual properties + property bool standalone: false + property var state: SelectionState + property string name // Component/File name + readonly property string _input: state.input + readonly property string input: !standalone ? _input.slice(prefix.length) : _input + readonly property string inputCleaned: input.toLowerCase().trim() + property string prefix + property bool active: !standalone ? input && _input.startsWith(prefix) : true + property bool valid: !standalone ? processed?.valid || false : true + property bool priority: !standalone ? valid && processed?.priority || false : true // please set priority to false if it's invalid + property string answer: processed?.answer || "" + property Component preview: Component { + IText { + // TODO: use proper active: false via Loader + visible: answer.length <= Launcher.maxPreviewLen + animate: true + text: answer + } + } + property string predictiveCompletion: processed?.predictiveCompletion || "" // would technically work with just from answer, but stuff like calc's answer wouldnt have anything to do with the input + property var processed: active ? process() : {} // cached process, thought qml would do that automatically :/ + // todo: change all to function + property var process: () => ({ + valid: valid, + priority: priority, + answer: answer, + preview: preview, + predictiveCompletion: predictiveCompletion + }) + // use the current IRect if valid is set + property var syncSelectionState: () => {} + // actions + property var up: () => ({ + top: true + }) + property var down: () => ({ + bottom: true + }) + property var prev: () => ({ + left: true + }) + property var next: () => ({ + right: true + }) + // true if not closed + property var exec: () => {} + property var home: () => ({ + top: true + }) + property var end: () => ({ + bottom: true + }) + property var pgup: () => ({ + top: true + }) + property var pgdn: () => ({ + bottom: true + }) + + function enter() { + trySetSelfPriority(); + // root or null hmm + state.selected = root; + } + + signal close + + function _exec() { + if (!exec()) + close(); + } + + function getSelfPriority() { + return state.priorities.indexOf(root); + } + + function isSelectedPriority() { + return state.selectedPriority === getSelfPriority(); + } + + function trySetSelfPriority() { + let self = getSelfPriority(); + // ??? why tf is it -1 not null or undefined this is why rust is better + if (self !== -1) + state.selectedPriority = self; + } + + function syncPriority() { + if (!standalone) { + if (state.priorities.includes(root) && !priority) + state.priorities = state.priorities.filter(x => x !== root); + else if (priority) + state.priorities = [...new Set(state.priorities), root]; + } else + state.priorities = [root]; + } + + // this doesn't change launcher size but only hides the actual content but the outside still think the content is there + visible: active + opacity: valid ? 1 : 0 + y: valid ? 0 : -Launcher.widgetHeight + + implicitWidth: valid ? Launcher.widgetWidth : 0 + implicitHeight: valid ? Launcher.widgetHeight : 0 + + radius: Launcher.widgetRadius + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetBgTransparency) // TODO when prioritized, highlight + + onPriorityChanged: { + // sync with state.priorities + Qt.callLater(() => { + syncPriority(); + }); + } + + Behavior on y { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + + HoverHandler { + onHoveredChanged: { + if (hovered) { + root.enter(); + root.syncSelectionState(); + } + } + } + + Component.onCompleted: { + Qt.callLater(() => { + syncPriority(); + }); + + // Qt.callLater(() => { + // syncSelectionState(); + // }); + } + + Component.onDestruction: { + state.priorities = state.priorities.filter(x => x !== root); + } } diff --git a/popups/launcher/components/IInnerComponent.qml b/popups/launcher/components/IInnerComponent.qml index 7bbc9f0..9f95592 100644 --- a/popups/launcher/components/IInnerComponent.qml +++ b/popups/launcher/components/IInnerComponent.qml @@ -5,30 +5,30 @@ import qs.components import qs.config ColumnLayout { - property bool fromParent: true - property alias titleBar: titleBar - anchors.fill: fromParent ? parent : undefined - spacing: 0 - IRect { - id: titleBar - Layout.fillWidth: true - Layout.fillHeight: false - visible: Launcher.showWidgetTitle && valid - height: childrenRect.height - width: parent.width - topLeftRadius: Launcher.widgetRadius - topRightRadius: Launcher.widgetRadius - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetTitleBgTransparency) + property bool fromParent: true + property alias titleBar: titleBar + anchors.fill: fromParent ? parent : undefined + spacing: 0 + IRect { + id: titleBar + Layout.fillWidth: true + Layout.fillHeight: false + visible: Launcher.showWidgetTitle && valid + height: childrenRect.height + width: parent.width + topLeftRadius: Launcher.widgetRadius + topRightRadius: Launcher.widgetRadius + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetTitleBgTransparency) - IText { - anchors { - top: parent.top - left: parent.left - leftMargin: Launcher.innerMargin - } - visible: valid - font.pixelSize: Launcher.widgetFontSize - text: name - } - } + IText { + anchors { + top: parent.top + left: parent.left + leftMargin: Launcher.innerMargin + } + visible: valid + font.pixelSize: Launcher.widgetFontSize + text: name + } + } } diff --git a/popups/launcher/components/ISmartCalc.qml b/popups/launcher/components/ISmartCalc.qml index 679bd5f..7f4302a 100644 --- a/popups/launcher/components/ISmartCalc.qml +++ b/popups/launcher/components/ISmartCalc.qml @@ -9,86 +9,86 @@ import qs.config import core IComponent { - id: root + id: root - property string symbol - required property bool category - required property string predictiveText + property string symbol + required property bool category + required property string predictiveText - property int cursorPosition: SelectionState.cursorPosition + property int cursorPosition: SelectionState.cursorPosition - preview: Component { - Icon { - text: symbol - } - } + preview: Component { + Icon { + text: symbol + } + } - onInputChanged: { - SmartCalc.reset_result(); - } + onInputChanged: { + SmartCalc.reset_result(); + } - process: function () { - SmartCalc.query(input); - const answer = SmartCalc.result; - // TODO: use answer property rn its circular dependent since it has to be valid which is the line below - const valid = SmartCalc.result && SmartCalc.result.length > 0 && category; - return { - valid, - priority: valid, - answer, - predictiveCompletion: ` ${predictiveText} ` + answer - }; - } - exec: function () { - Clip.copy(answer); - } + process: function () { + SmartCalc.query(input); + const answer = SmartCalc.result; + // TODO: use answer property rn its circular dependent since it has to be valid which is the line below + const valid = SmartCalc.result && SmartCalc.result.length > 0 && category; + return { + valid, + priority: valid, + answer, + predictiveCompletion: ` ${predictiveText} ` + answer + }; + } + exec: function () { + Clip.copy(answer); + } - IInnerComponent { - RowLayout { - spacing: 0 - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: parent.height + IInnerComponent { + RowLayout { + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height - Item { - Layout.fillWidth: true - IText { - anchors.centerIn: parent - elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight - width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) - clip: true - renderType: Text.CurveRendering - visible: valid - text: input - font { - pixelSize: Launcher.widgetFontSize - bold: root.isSelectedPriority() - } - } - } + Item { + Layout.fillWidth: true + IText { + anchors.centerIn: parent + elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + clip: true + renderType: Text.CurveRendering + visible: valid + text: input + font { + pixelSize: Launcher.widgetFontSize + bold: root.isSelectedPriority() + } + } + } - IText { - visible: valid - text: "→" - font.pixelSize: Launcher.widgetFontSize - font.bold: root.isSelectedPriority() - } + IText { + visible: valid + text: "→" + font.pixelSize: Launcher.widgetFontSize + font.bold: root.isSelectedPriority() + } - Item { - Layout.fillWidth: true - IText { - animate: true - anchors.centerIn: parent - width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) - renderType: Text.CurveRendering - visible: valid - text: valid ? answer : '' - font { - pixelSize: Launcher.widgetFontSize - bold: root.isSelectedPriority() - } - } - } - } - } + Item { + Layout.fillWidth: true + IText { + animate: true + anchors.centerIn: parent + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + renderType: Text.CurveRendering + visible: valid + text: valid ? answer : '' + font { + pixelSize: Launcher.widgetFontSize + bold: root.isSelectedPriority() + } + } + } + } + } } diff --git a/popups/launcher/components/SelectionState.qml b/popups/launcher/components/SelectionState.qml index 5b80b2e..3fc1f79 100644 --- a/popups/launcher/components/SelectionState.qml +++ b/popups/launcher/components/SelectionState.qml @@ -5,46 +5,46 @@ import QtQuick // FIXME: fix some selection not show/updated when deselected i.e. "pi" from app back to calc wont visually show deselection of last app // FIXME: icon/answer not updated when changed to non first priority Singleton { - id: root - - PersistentProperties { - id: persist - reloadableId: "launcherSelectionState" - - property string input: "" - property int cursorPosition: 0 - property int selectedPriority: 0 - } - - property Item selected: null - property alias input: persist.input - property alias cursorPosition: persist.cursorPosition - property var priorities: [] - property alias selectedPriority: persist.selectedPriority - property var widgets: [] - signal syncSelectionState - - onSelectedPriorityChanged: { - priorities[selectedPriority]?.enter(); - priorities[selectedPriority]?.syncSelectionState(); - } - - // order them based on widgets(which came from order from config) - onPrioritiesChanged: { - for (let i = priorities.length - 1; i >= 0; i--) - if (!priorities[i] || priorities[i].priority !== true) - priorities.splice(i, 1); - - priorities.sort((a, b) => (widgets.indexOf(a) - widgets.indexOf(b)) || 0); - - Qt.callLater(function step() { - if (!priorities[selectedPriority] && selectedPriority > 0) { - selectedPriority--; - Qt.callLater(step); - } else { - priorities[selectedPriority]?.enter(); - priorities[selectedPriority]?.syncSelectionState(); - } - }); - } + id: root + + PersistentProperties { + id: persist + reloadableId: "launcherSelectionState" + + property string input: "" + property int cursorPosition: 0 + property int selectedPriority: 0 + } + + property Item selected: null + property alias input: persist.input + property alias cursorPosition: persist.cursorPosition + property var priorities: [] + property alias selectedPriority: persist.selectedPriority + property var widgets: [] + signal syncSelectionState + + onSelectedPriorityChanged: { + priorities[selectedPriority]?.enter(); + priorities[selectedPriority]?.syncSelectionState(); + } + + // order them based on widgets(which came from order from config) + onPrioritiesChanged: { + for (let i = priorities.length - 1; i >= 0; i--) + if (!priorities[i] || priorities[i].priority !== true) + priorities.splice(i, 1); + + priorities.sort((a, b) => (widgets.indexOf(a) - widgets.indexOf(b)) || 0); + + Qt.callLater(function step() { + if (!priorities[selectedPriority] && selectedPriority > 0) { + selectedPriority--; + Qt.callLater(step); + } else { + priorities[selectedPriority]?.enter(); + priorities[selectedPriority]?.syncSelectionState(); + } + }); + } } diff --git a/popups/launcher/components/Spell.qml b/popups/launcher/components/Spell.qml index c09a705..b7c1aa6 100644 --- a/popups/launcher/components/Spell.qml +++ b/popups/launcher/components/Spell.qml @@ -8,232 +8,232 @@ import qs.services import qs.config IComponent { - id: root - - property list<string> aspell - property int selectedIndex: -1 - - name: "Spell" - prefix: name.toLowerCase() + ' ' - - onValidChanged: { - if (valid) - return; - - aspell = []; - proc.running = false; - selectedIndex = -1; - return; - } - - onAspellChanged: { - Qt.callLater(() => { - if (selectedIndex !== -1) { - syncSelectionState(); - return; - } - selectedIndex = aspell.length > 0 ? 0 : -1; - if (loader.item && loader.item.flickable) - loader.item.flickable.contentY = 0; - }); - } - - onSelectedIndexChanged: { - Qt.callLater(() => { - if (!loader.view || selectedIndex < 0 || selectedIndex >= loader.view.count) - return; - - syncSelectionState(); - loader.view.currentIndex = selectedIndex; - loader.view.positionViewAtIndex(selectedIndex, GridView.Contain); - }); - } - - process: function () { - Qt.callLater(() => { - proc.running = true; - }); - - const isValid = selectedIndex >= 0 && aspell.length > 0; - const selected = isValid ? aspell[selectedIndex] : ""; - const answer = selected === "*" ? input : selected; - - return { - valid: isValid, - priority: isValid, - answer: answer || "", - predictiveCompletion: answer?.slice(input.length) || "" - }; - } - - home: function () { - if (loader.view.count === 0) - return true; - selectedIndex = -1; - selectedIndex = 0; - } - end: function () { - if (loader.view.count === 0) - return true; - selectedIndex = loader.view.count; - selectedIndex = loader.view.count - 1; - } - - prev: function () { - if (selectedIndex <= 0) - return true; - selectedIndex--; - } - - next: function () { - if (selectedIndex + 1 >= aspell.length) - return true; - selectedIndex++; - } - - up: function () { - const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6)))); - if (selectedIndex - cols < 0) - return true; - selectedIndex -= cols; - } - - down: function () { - const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6)))); - if (selectedIndex + cols >= aspell.length) - return true; - selectedIndex += cols; - } - - exec: function () { - if (selectedIndex >= 0) - if (aspell[selectedIndex] !== "*") - Clip.copy(aspell[selectedIndex]); - else - Clip.copy(input); - } - - syncSelectionState: function () { - Qt.callLater(() => { - if (!loader.view || selectedIndex < 0 || selectedIndex >= loader.view.count || !root.visible) - return; - - trySetSelfPriority(); - state.selected = loader.view.itemAtIndex(selectedIndex); - }); - } - - IInnerComponent { - id: innerLoader - Loader { - id: loader - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: parent.height - sourceComponent: aspell[0] === "*" ? correct : show - property var view: null - - readonly property Component show: Component { - Item { - property alias flickable: grid - - GridView { - id: grid - anchors.fill: parent - clip: true - boundsBehavior: Flickable.StopAtBounds - cacheBuffer: 0 - model: aspell - interactive: true - flow: GridView.FlowLeftToRight - snapMode: GridView.NoSnap - - cellWidth: Math.floor(width / Math.max(1, Math.floor(width / Math.max(1, Math.round(Launcher.widgetFontSize * 6))))) - cellHeight: Math.ceil(Launcher.widgetFontSize * 1.6) + Launcher.innerMargin * 2 - - delegate: Item { - required property var modelData - required property int index - - width: grid.cellWidth - height: chipContent.implicitHeight + Launcher.innerMargin * 2 - scale: index === selectedIndex ? 1.03 : 0.97 - - IRect { - anchors.fill: parent - color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) - radius: Launcher.widgetRadius - - IText { - id: chipContent - anchors.centerIn: parent - text: modelData - font.pixelSize: Launcher.widgetFontSize - elide: Text.ElideRight - color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - root.selectedIndex = index; - } - onPressed: { - root._exec(); - } - } - } - - Behavior on scale { - ISpringAnimation {} - } - } - - Component.onCompleted: { - loader.view = grid; - } - } - } - } - - readonly property Component correct: Component { - IRect { - Layout.fillWidth: true - color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.widgetBgTransparency / 4) - radius: Launcher.widgetRadius - IText { - anchors.centerIn: parent - text: "Correct" - font.pixelSize: Launcher.widgetFontSize - anchors.horizontalCenter: parent.horizontalCenter - } - } - } - } - } - - Process { - id: proc - command: ["sh", "-c", "echo " + input + " | aspell -a"] - property bool found: false - stdout: SplitParser { - onRead: data => { - if (data.startsWith("&")) { - proc.found = true; - aspell = data.split(/\W+/).filter(input => /^[A-Za-z]+$/.test(input)).slice(1); - } else if (data.startsWith("*")) { - proc.found = true; - aspell = ["*"]; - } - } - } - onStarted: { - found = false; - } - onExited: { - if (!found) - aspell = []; - } - } + id: root + + property list<string> aspell + property int selectedIndex: -1 + + name: "Spell" + prefix: name.toLowerCase() + ' ' + + onValidChanged: { + if (valid) + return; + + aspell = []; + proc.running = false; + selectedIndex = -1; + return; + } + + onAspellChanged: { + Qt.callLater(() => { + if (selectedIndex !== -1) { + syncSelectionState(); + return; + } + selectedIndex = aspell.length > 0 ? 0 : -1; + if (loader.item && loader.item.flickable) + loader.item.flickable.contentY = 0; + }); + } + + onSelectedIndexChanged: { + Qt.callLater(() => { + if (!loader.view || selectedIndex < 0 || selectedIndex >= loader.view.count) + return; + + syncSelectionState(); + loader.view.currentIndex = selectedIndex; + loader.view.scrollToIndex(selectedIndex, GridView.Contain); + }); + } + + process: function () { + Qt.callLater(() => { + proc.running = true; + }); + + const isValid = selectedIndex >= 0 && aspell.length > 0; + const selected = isValid ? aspell[selectedIndex] : ""; + const answer = selected === "*" ? input : selected; + + return { + valid: isValid, + priority: isValid, + answer: answer || "", + predictiveCompletion: answer?.slice(input.length) || "" + }; + } + + home: function () { + if (loader.view.count === 0) + return true; + selectedIndex = -1; + selectedIndex = 0; + } + end: function () { + if (loader.view.count === 0) + return true; + selectedIndex = loader.view.count; + selectedIndex = loader.view.count - 1; + } + + prev: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + + next: function () { + if (selectedIndex + 1 >= aspell.length) + return true; + selectedIndex++; + } + + up: function () { + const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6)))); + if (selectedIndex - cols < 0) + return true; + selectedIndex -= cols; + } + + down: function () { + const cols = Math.max(1, Math.floor(innerLoader.width / Math.max(1, Math.round(Launcher.widgetFontSize * 6)))); + if (selectedIndex + cols >= aspell.length) + return true; + selectedIndex += cols; + } + + exec: function () { + if (selectedIndex >= 0) + if (aspell[selectedIndex] !== "*") + Clip.copy(aspell[selectedIndex]); + else + Clip.copy(input); + } + + syncSelectionState: function () { + Qt.callLater(() => { + if (!loader.view || selectedIndex < 0 || selectedIndex >= loader.view.count || !root.visible) + return; + + trySetSelfPriority(); + state.selected = loader.view.itemAtIndex(selectedIndex); + }); + } + + IInnerComponent { + id: innerLoader + Loader { + id: loader + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + sourceComponent: aspell[0] === "*" ? correct : show + property var view: null + + readonly property Component show: Component { + Item { + property alias flickable: grid + + GridView { + id: grid + anchors.fill: parent + clip: true + boundsBehavior: Flickable.StopAtBounds + cacheBuffer: 0 + model: aspell + interactive: true + flow: GridView.FlowLeftToRight + snapMode: GridView.NoSnap + + cellWidth: Math.floor(width / Math.max(1, Math.floor(width / Math.max(1, Math.round(Launcher.widgetFontSize * 6))))) + cellHeight: Math.ceil(Launcher.widgetFontSize * 1.6) + Launcher.innerMargin * 2 + + delegate: Item { + required property var modelData + required property int index + + width: grid.cellWidth + height: chipContent.implicitHeight + Launcher.innerMargin * 2 + scale: index === selectedIndex ? 1.03 : 0.97 + + IRect { + anchors.fill: parent + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + radius: Launcher.widgetRadius + + IText { + id: chipContent + anchors.centerIn: parent + text: modelData + font.pixelSize: Launcher.widgetFontSize + elide: Text.ElideRight + color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + root.selectedIndex = index; + } + onPressed: { + root._exec(); + } + } + } + + Behavior on scale { + ISpringAnimation {} + } + } + + Component.onCompleted: { + loader.view = grid; + } + } + } + } + + readonly property Component correct: Component { + IRect { + Layout.fillWidth: true + color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.widgetBgTransparency / 4) + radius: Launcher.widgetRadius + IText { + anchors.centerIn: parent + text: "Correct" + font.pixelSize: Launcher.widgetFontSize + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + } + + Process { + id: proc + command: ["sh", "-c", "echo " + input + " | aspell -a"] + property bool found: false + stdout: SplitParser { + onRead: data => { + if (data.startsWith("&")) { + proc.found = true; + aspell = data.split(/\W+/).filter(input => /^[A-Za-z]+$/.test(input)).slice(1); + } else if (data.startsWith("*")) { + proc.found = true; + aspell = ["*"]; + } + } + } + onStarted: { + found = false; + } + onExited: { + if (!found) + aspell = []; + } + } } diff --git a/qml_plugin/build.rs b/qml_plugin/build.rs index 76df4b7..25e03ae 100644 --- a/qml_plugin/build.rs +++ b/qml_plugin/build.rs @@ -6,19 +6,14 @@ fn main() { let version_script = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("qml_plugin.version"); - println!( - "cargo:rustc-cdylib-link-arg=-Wl,--version-script={}", - version_script.display() - ); + println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script={}", version_script.display()); CxxQtBuilder::new_qml_module(QmlModule::new(NAME).plugin_type(PluginType::Dynamic)) .qt_module("Qml") - .files( - [ - "src/launcher/smartcalc.rs", - "src/launcher/mathcalc.rs", - "src/launcher/duckduckgo.rs", - ], - ) + .files([ + "src/launcher/smartcalc.rs", + "src/launcher/mathcalc.rs", + "src/launcher/duckduckgo.rs", + ]) .build(); } diff --git a/qml_plugin/src/launcher/duckduckgo.rs b/qml_plugin/src/launcher/duckduckgo.rs index 16c9102..0375cdf 100644 --- a/qml_plugin/src/launcher/duckduckgo.rs +++ b/qml_plugin/src/launcher/duckduckgo.rs @@ -16,19 +16,10 @@ mod qobject { #[qobject] #[qml_element] #[qml_singleton] - #[qproperty( - bool, ok - )] - #[qproperty( - QString, title - )] - #[qproperty( - QString, - description_html - )] - #[qproperty( - QString, image - )] + #[qproperty(bool, ok)] + #[qproperty(QString, title)] + #[qproperty(QString, description_html)] + #[qproperty(QString, image)] type DuckDuckGo = super::DuckDuckGoRs; #[qinvokable] @@ -61,60 +52,37 @@ pub struct DuckDuckGoRs { impl qobject::DuckDuckGo { fn query(self: Pin<&mut Self>, input: QString) { let qt_thread = self.qt_thread(); - self.rust_mut() - .executor - .spawn( - async move { - let qinput = input.to_string(); - let result: Result<QmlDuckDuckGo, _> = duckduckgo::query(&qinput) - .await - .map(|r| r.into()); - // consider just unwrapping since this is in a separate thread and is already - // end of life anw - _ = qt_thread - .queue( - move |mut qo| match result { - Ok(r) => { - if (r - .title - .to_lower() - == input.to_lower() - || r.title - .is_empty()) - && r.description_html - .is_empty() - && r.image - .is_empty() - { - qo.as_mut() - .set_ok(false); - } else { - qo.as_mut() - .set_ok(true); - qo.as_mut() - .set_title(r.title); - qo.as_mut() - .set_description_html(r.description_html); - qo.as_mut() - .set_image(r.image); - } - } - Err(_) => qo.set_ok(false), - }, - ) - .unwrap_print(); - }, - ); + self.rust_mut().executor.spawn(async move { + let qinput = input.to_string(); + let result: Result<QmlDuckDuckGo, _> = + duckduckgo::query(&qinput).await.map(|r| r.into()); + // consider just unwrapping since this is in a separate thread and + // is already end of life anw + _ = qt_thread + .queue(move |mut qo| match result { + Ok(r) => { + if (r.title.to_lower() == input.to_lower() || r.title.is_empty()) + && r.description_html.is_empty() + && r.image.is_empty() + { + qo.as_mut().set_ok(false); + } else { + qo.as_mut().set_ok(true); + qo.as_mut().set_title(r.title); + qo.as_mut().set_description_html(r.description_html); + qo.as_mut().set_image(r.image); + } + } + Err(_) => qo.set_ok(false), + }) + .unwrap_print(); + }); } fn reset(mut self: Pin<&mut Self>) { - self.as_mut() - .set_ok(false); - self.as_mut() - .set_title("".into()); - self.as_mut() - .set_description_html("".into()); - self.as_mut() - .set_image("".into()); + self.as_mut().set_ok(false); + self.as_mut().set_title("".into()); + self.as_mut().set_description_html("".into()); + self.as_mut().set_image("".into()); } } diff --git a/qml_plugin/src/launcher/mathcalc.rs b/qml_plugin/src/launcher/mathcalc.rs index d29239e..601dc3b 100644 --- a/qml_plugin/src/launcher/mathcalc.rs +++ b/qml_plugin/src/launcher/mathcalc.rs @@ -22,9 +22,7 @@ mod qobject { #[qobject] #[qml_element] #[qml_singleton] - #[qproperty( - QVariant, result - )] + #[qproperty(QVariant, result)] type MathCalc = super::MathCalcRs; #[qinvokable] @@ -63,61 +61,34 @@ impl Default for MathCalcRs { impl qobject::MathCalc { fn query(mut self: Pin<&mut Self>, input: QString) { let qt_thread = self.qt_thread(); - let ctx = Arc::clone( - &self - .as_mut() - .rust_mut() - .ctx, - ); + let ctx = Arc::clone(&self.as_mut().rust_mut().ctx); // TODO: consider moving to spawn_blocking - self.rust_mut() - .executor - .spawn( - async move { - let input = input.to_string(); - let result = parser::eval( - &mut *ctx - .lock() - .await, - &input, - // TODO: move to const/config - 53, - ) - .map_or_default( - |r| { - r.map_or_default( - |r| { - let r: QString = r - // to display scientific notation properly - // other ones show both version without e or shows *10^_ - // and have a = prefix - // to_string_big will result in 1/2=5.0000000000000000e − 1 - .to_string() - .into(); - if r != input.into() { - QVariant::from(&r) - } else { - QVariant::default() - } - }, - ) - }, - ); - _ = qt_thread - .queue( - |mut qo| { - qo.as_mut() - .set_result(result) - }, - ) - .unwrap_print(); - }, - ); + self.rust_mut().executor.spawn(async move { + let input = input.to_string(); + let result = parser::eval( + &mut *ctx.lock().await, + &input, + // TODO: move to const/config + 53, + ) + .map_or_default(|r| { + r.map_or_default(|r| { + let r: QString = r + // to display scientific notation properly + // other ones show both version without e or shows *10^_ + // and have a = prefix + // to_string_big will result in 1/2=5.0000000000000000e − 1 + .to_string() + .into(); + if r != input.into() { QVariant::from(&r) } else { QVariant::default() } + }) + }); + _ = qt_thread.queue(|mut qo| qo.as_mut().set_result(result)).unwrap_print(); + }); } fn reset(mut self: Pin<&mut Self>) { - self.as_mut() - .reset_result(); + self.as_mut().reset_result(); self.reset_ctx(); } @@ -126,13 +97,10 @@ impl qobject::MathCalc { } fn reset_ctx(mut self: Pin<&mut Self>) { - self.as_mut() - .rust_mut() - .ctx = Arc::new(Mutex::new(Self::new_ctx())) + self.as_mut().rust_mut().ctx = Arc::new(Mutex::new(Self::new_ctx())) } fn reset_result(mut self: Pin<&mut Self>) { - self.as_mut() - .set_result(QVariant::default()) + self.as_mut().set_result(QVariant::default()) } } diff --git a/qml_plugin/src/launcher/smartcalc.rs b/qml_plugin/src/launcher/smartcalc.rs index c23ecfa..9b5f05a 100644 --- a/qml_plugin/src/launcher/smartcalc.rs +++ b/qml_plugin/src/launcher/smartcalc.rs @@ -18,17 +18,9 @@ mod qobject { #[qobject] #[qml_element] #[qml_singleton] - #[qproperty( - QVariant, result - )] - #[qproperty( - ResultType, - result_type - )] - #[qproperty( - QVariant, - cached_query - )] + #[qproperty(QVariant, result)] + #[qproperty(ResultType, result_type)] + #[qproperty(QVariant, cached_query)] type SmartCalc = super::SmartCalcRs; #[qinvokable] @@ -89,64 +81,43 @@ impl qobject::SmartCalc { if self.cached_query == (&input).into() { return; } - self.as_mut() - .set_cached_query((&input.clone()).into()); + self.as_mut().set_cached_query((&input.clone()).into()); let qt_thread = self.qt_thread(); - self.as_mut() - .rust_mut() - .executor - .spawn( - async move { - let result = smart_calculator::calculate( - &input.to_string(), - None, + self.as_mut().rust_mut().executor.spawn(async move { + let result = smart_calculator::calculate(&input.to_string(), None).await; + _ = qt_thread + .queue(|mut qo| { + qo.as_mut().set_result_type(match &result { + Ok(value) => (&value.res_type).into(), + Err(_) => qobject::ResultType::Unset, + }); + qo.as_mut().set_result( + // TODO: do the calc outside of ts main qt ui render + // thread.(all *.rs) + match result { + Ok(value) => { + if value.formatted == value.input { + QVariant::default() + } else { + let result: QString = value.formatted.into(); + QVariant::from(&result) + } + } + Err(_) => QVariant::default(), + }, ) - .await; - _ = qt_thread - .queue( - |mut qo| { - qo.as_mut() - .set_result_type( - match &result { - Ok(value) => (&value.res_type).into(), - Err(_) => qobject::ResultType::Unset, - }, - ); - qo.as_mut() - .set_result( - // TODO: do the calc outside of ts main qt ui render - // thread.(all *.rs) - match result { - Ok(value) => { - if value.formatted == value.input { - QVariant::default() - } else { - let result: QString = value - .formatted - .into(); - QVariant::from(&result) - } - } - Err(_) => QVariant::default(), - }, - ) - }, - ) - .unwrap_print(); - }, - ); + }) + .unwrap_print(); + }); } fn reset(mut self: Pin<&mut Self>) { - self.as_mut() - .set_cached_query(QVariant::default()); + self.as_mut().set_cached_query(QVariant::default()); self.reset_result(); } fn reset_result(mut self: Pin<&mut Self>) { - self.as_mut() - .set_result(QVariant::default()); - self.as_mut() - .set_result_type(qobject::ResultType::Unset); + self.as_mut().set_result(QVariant::default()); + self.as_mut().set_result_type(qobject::ResultType::Unset); } } diff --git a/qml_plugin/src/lib.rs b/qml_plugin/src/lib.rs index f9f095e..0f1411c 100644 --- a/qml_plugin/src/lib.rs +++ b/qml_plugin/src/lib.rs @@ -2,14 +2,8 @@ use std::sync::LazyLock; use tokio::task; -static RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new( - || { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - }, -); +static RUNTIME: LazyLock<tokio::runtime::Runtime> = + LazyLock::new(|| tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()); /// for example duckduckgo we wanna cancel first request if user types new query /// using this guarentee that only one request is running at a time or at least @@ -32,10 +26,7 @@ impl ExclusiveExecutor { F: Future<Output = ()> + Send + 'static, F::Output: Send + 'static, { - if let Some(handle) = self - .handle - .take() - { + if let Some(handle) = self.handle.take() { handle.abort(); } diff --git a/qml_plugin/src/web/duckduckgo.rs b/qml_plugin/src/web/duckduckgo.rs index fcfa633..6b4edae 100644 --- a/qml_plugin/src/web/duckduckgo.rs +++ b/qml_plugin/src/web/duckduckgo.rs @@ -20,21 +20,9 @@ pub struct DuckDuckGoContent { impl From<DuckDuckGoContent> for QmlDuckDuckGo { fn from(value: DuckDuckGoContent) -> Self { Self { - title: value - .title - .into(), - description_html: value - .description_html - .into(), - image: value - .image - .map( - |u| { - u.to_string() - .into() - }, - ) - .unwrap_or_default(), + title: value.title.into(), + description_html: value.description_html.into(), + image: value.image.map(|u| u.to_string().into()).unwrap_or_default(), } } } @@ -86,79 +74,45 @@ pub async fn query(input: &str) -> anyhow::Result<DuckDuckGoContent> { let resp = Browser::new() .get_api_response( input, - Some( - &SearchParams::new() - .safe_search(SafeSearch::Off) - .full_urls(Toggle::On), - ), + Some(&SearchParams::new().safe_search(SafeSearch::Off).full_urls(Toggle::On)), ) .await?; - let infobox = match resp - .info_box - .map(serde_json::from_value::<Infobox>) - { + let infobox = match resp.info_box.map(serde_json::from_value::<Infobox>) { Some(Ok(o)) => Some(o), _ => None, }; - let alt_desc = resp - .entity - .unwrap_or( - match infobox { - Some(i) => i - .iter() - .find(|i| i.data_type == "wd_description" || i.data_type == "official_website") - .map_or( - String::new(), - |i| { - if i.data_type == "official_website" { - i.label - .clone() - } else { - match i.value { - InfoboxValue::Entity(_) => unreachable!(), - InfoboxValue::String(ref s) => s.to_string(), - } - } - }, - ), - // TODO: find better way to get title from infobox - None => String::new(), - }, - ); + let alt_desc = resp.entity.unwrap_or(match infobox { + Some(i) => i + .iter() + .find(|i| i.data_type == "wd_description" || i.data_type == "official_website") + .map_or(String::new(), |i| { + if i.data_type == "official_website" { + i.label.clone() + } else { + match i.value { + InfoboxValue::Entity(_) => unreachable!(), + InfoboxValue::String(ref s) => s.to_string(), + } + } + }), + // TODO: find better way to get title from infobox + None => String::new(), + }); - Ok( - DuckDuckGoContent { - title: resp - .heading - .filter(|h| !h.is_empty()) - .unwrap_or( - resp.definition - .filter(|d| !d.is_empty()) - .unwrap_or(alt_desc.clone()), - ), - description_html: resp - .answer - .filter(|a| !a.is_empty()) - .unwrap_or( - resp.r#abstract - .filter(|a| !a.is_empty()) - .unwrap_or(alt_desc), - ), - // TODO: try also Results[*].Icon.URL and RelatedTopics[*].Icon.URL - image: resp - .image - .filter(|i| !i.is_empty()) - .and_then( - |i| { - if !i.is_empty() { - Url::parse(&format!("{DUCKDUCKGO}{i}",)).ok() - } else { - None - } - }, - ), - }, - ) + Ok(DuckDuckGoContent { + title: resp + .heading + .filter(|h| !h.is_empty()) + .unwrap_or(resp.definition.filter(|d| !d.is_empty()).unwrap_or(alt_desc.clone())), + description_html: resp + .answer + .filter(|a| !a.is_empty()) + .unwrap_or(resp.r#abstract.filter(|a| !a.is_empty()).unwrap_or(alt_desc)), + // TODO: try also Results[*].Icon.URL and RelatedTopics[*].Icon.URL + image: resp.image.filter(|i| !i.is_empty()).and_then(|i| { + if !i.is_empty() { Url::parse(&format!("{DUCKDUCKGO}{i}",)).ok() } else { None } + }), + }) } diff --git a/qml_plugin/src/web/google.rs b/qml_plugin/src/web/google.rs index e69de29..8b13789 100644 --- a/qml_plugin/src/web/google.rs +++ b/qml_plugin/src/web/google.rs @@ -0,0 +1 @@ + diff --git a/services/AppSearch.qml b/services/AppSearch.qml index d56213f..b8294b6 100644 --- a/services/AppSearch.qml +++ b/services/AppSearch.qml @@ -6,119 +6,119 @@ import qs.config import qs.components Searchable { - id: root - - list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name)) - scoreThreshold: General.appSearchFuzzySearchThreshold - algorithm: Searchable.SearchAlgorithm.Levendist - keys: ["name", "genericName", "execString", "keywords", "comment", "categories", "icon"] - - property var substitutions: ({ - "code-url-handler": "visual-studio-code", - "Code": "visual-studio-code", - "gnome-tweaks": "org.gnome.tweaks", - "pavucontrol-qt": "pavucontrol", - "wps": "wps-office2019-kprometheus", - "wpsoffice": "wps-office2019-kprometheus", - "footclient": "foot", - "zen": "zen-browser", - "brave-browser": "brave-desktop" - }) - - property var regexSubstitutions: [ - { - regex: /^steam_app_(\d+)$/, - replace: "steam_icon_$1" - }, - { - regex: /Minecraft.*/, - replace: "minecraft" - }, - { - regex: /.*polkit.*/, - replace: "system-lock-screen" - }, - { - regex: /gcr.prompter/, - replace: "system-lock-screen" - } - ] - - property var iconExistsCache: ({}) - property var guessIconCache: ({}) - - function iconExists(iconName) { - if (!iconName || iconName.length === 0) { - return false; - } - if (root.iconExistsCache.hasOwnProperty(iconName)) { - return root.iconExistsCache[iconName]; - } - const exists = Quickshell.iconPath(iconName, true).length > 0 && !iconName.includes("image-missing"); - root.iconExistsCache[iconName] = exists; - return exists; - } - - function guessIcon(str) { - if (!str || str.length === 0) { - return "image-missing"; - } - if (root.guessIconCache[str]) { - return root.guessIconCache[str]; - } - - var result; - - if (substitutions[str]) { - result = substitutions[str]; - } else { - result = null; - for (var i = 0; i < regexSubstitutions.length; i++) { - var { - regex, - replace - } = regexSubstitutions[i]; - var replaced = str.replace(regex, replace); - if (replaced !== str) { - result = replaced; - break; - } - } - } - - if (!result && root.iconExists(str)) { - result = str; - } - - if (!result) { - var guessStr = str.split(".").slice(-1)[0].toLowerCase(); - if (root.iconExists(guessStr)) { - result = guessStr; - } - } - - if (!result) { - var kebab = str.toLowerCase().replace(/\s+/g, "-"); - if (root.iconExists(kebab)) { - result = kebab; - } - } - - if (!result) { - const matches = root.query(str); - if (matches.length > 0) { - const candidate = matches[0].icon; - if (root.iconExists(candidate)) { - result = candidate; - } - } - } - - if (!result) { - result = str; - } - - root.guessIconCache[str] = result; - return result; - } + id: root + + list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name)) + scoreThreshold: General.appSearchFuzzySearchThreshold + algorithm: Searchable.SearchAlgorithm.Levendist + keys: ["name", "genericName", "execString", "keywords", "comment", "categories", "icon"] + + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot", + "zen": "zen-browser", + "brave-browser": "brave-desktop" + }) + + property var regexSubstitutions: [ + { + regex: /^steam_app_(\d+)$/, + replace: "steam_icon_$1" + }, + { + regex: /Minecraft.*/, + replace: "minecraft" + }, + { + regex: /.*polkit.*/, + replace: "system-lock-screen" + }, + { + regex: /gcr.prompter/, + replace: "system-lock-screen" + } + ] + + property var iconExistsCache: ({}) + property var guessIconCache: ({}) + + function iconExists(iconName) { + if (!iconName || iconName.length === 0) { + return false; + } + if (root.iconExistsCache.hasOwnProperty(iconName)) { + return root.iconExistsCache[iconName]; + } + const exists = Quickshell.iconPath(iconName, true).length > 0 && !iconName.includes("image-missing"); + root.iconExistsCache[iconName] = exists; + return exists; + } + + function guessIcon(str) { + if (!str || str.length === 0) { + return "image-missing"; + } + if (root.guessIconCache[str]) { + return root.guessIconCache[str]; + } + + var result; + + if (substitutions[str]) { + result = substitutions[str]; + } else { + result = null; + for (var i = 0; i < regexSubstitutions.length; i++) { + var { + regex, + replace + } = regexSubstitutions[i]; + var replaced = str.replace(regex, replace); + if (replaced !== str) { + result = replaced; + break; + } + } + } + + if (!result && root.iconExists(str)) { + result = str; + } + + if (!result) { + var guessStr = str.split(".").slice(-1)[0].toLowerCase(); + if (root.iconExists(guessStr)) { + result = guessStr; + } + } + + if (!result) { + var kebab = str.toLowerCase().replace(/\s+/g, "-"); + if (root.iconExists(kebab)) { + result = kebab; + } + } + + if (!result) { + const matches = root.query(str); + if (matches.length > 0) { + const candidate = matches[0].icon; + if (root.iconExists(candidate)) { + result = candidate; + } + } + } + + if (!result) { + result = str; + } + + root.guessIconCache[str] = result; + return result; + } } diff --git a/services/Clip.qml b/services/Clip.qml index 2289dd2..4960dd9 100644 --- a/services/Clip.qml +++ b/services/Clip.qml @@ -11,251 +11,251 @@ import qs.config import "../utils/string_utils.js" as StringUtils Searchable { - id: root - - key: "data" - algorithm: General.defaultSearchAlgorithm - - property list<string> _clipHist: [] - property list<string> _clipImg: [] - property list<string> _clipImgIds: _clipImg.map(p => p.split("/").pop().split(".").shift()) - property var _clipMetadata: ({}) - property var _clipDecoded: ({}) - property int _pendingReorderId: -1 - - function getType(text: string): string { - if (!text) - return; - if (text.startsWith("http://") || text.startsWith("https://")) - return "web"; - if (text.startsWith("file://")) - return "file"; - - return "text"; - } - - list: { - if (!_clipHist?.length) - return []; - - const imgMap = _clipImgIds.slice(0, Math.min(_clipImg.length, _clipImgIds.length)).reduce((map, id, i) => { - if (id) - map[id] = _clipImg[i]; - return map; - }, {}); - - return _clipHist.filter(raw => raw).map((raw, idx) => { - const tab = raw.indexOf("\t"); - let idNum = -1; - let payload = raw; - - if (tab > 0) { - const n = parseInt(raw.slice(0, tab), 10); - if (!Number.isNaN(n)) { - idNum = n; - payload = raw.slice(tab + 1); - } - } - - const imgPath = idNum >= 0 ? imgMap[idNum] || "" : ""; - const isImg = !!imgPath; - const metadata = _clipMetadata[idNum] || {}; - - return { - index: idNum, - type: isImg ? "image" : root.getType(payload), - data: isImg ? imgPath : payload, - raw: raw, - timestamp: metadata.timestamp || "", - appId: metadata.appId || "", - appTitle: metadata.appTitle || "", - appIcon: Quickshell.iconPath(AppSearch.guessIcon(metadata.appId), "image-missing"), - image: imgPath, - decoded: root._clipDecoded[idx] || "" - }; - }); - } - - Persistent { - id: clipMetaStore - filePath: Directories.clipHistMetaDataPath - adapter: JsonAdapter { - property var metadata: ({}) - } - fileView.onLoaded: { - const meta = adapter.metadata; - for (const k in meta) { - if (meta[k].timestamp && typeof meta[k].timestamp === "string") { - meta[k].timestamp = new Date(meta[k].timestamp); - } - } - root._clipMetadata = meta; - } - } - - on_ClipMetadataChanged: { - clipMetaStore.adapter.metadata = _clipMetadata; - } - - function transformSearch(search) { - return search.toLowerCase(); - } - - function decodeAndCopy(text) { - const tab = text.indexOf("\t"); - if (tab > 0) { - const idStr = text.slice(0, tab); - const n = parseInt(idStr, 10); - if (!Number.isNaN(n)) - _pendingReorderId = n; - } - Quickshell.execDetached(["sh", "-c", `cliphist decode '${StringUtils.shellSingleQuoteEscape(text)}' | wl-copy`]); - } - - function copy(text) { - _pendingReorderId = -1; - Quickshell.execDetached(["wl-copy", text]); - } - - function _update() { - clipProc.running = true; - clipDecodeProc.running = true; - imgProc.running = true; - } - - function _captureMetadata() { - const now = new Date(); - const activeToplevel = ToplevelManager.activeToplevel; - - Qt.callLater(() => { - clipProc.running = true; - clipProc.stdout.onceFinished = () => { - const lines = clipProc.stdout.text.split(/\r?\n/); - const firstLine = lines[0] || ""; - const tab = firstLine.indexOf("\t"); - - if (tab > 0) { - const idStr = firstLine.slice(0, tab); - const n = parseInt(idStr, 10); - if (!Number.isNaN(n)) { - if (_clipMetadata[idStr]) { - _pendingReorderId = -1; - return; - } - - let newMetadata = {}; - for (var k in _clipMetadata) { - if (_clipMetadata.hasOwnProperty(k)) - newMetadata[k] = _clipMetadata[k]; - } - - if (_pendingReorderId >= 0) { - if (_clipMetadata[_pendingReorderId]) { - newMetadata[idStr] = _clipMetadata[_pendingReorderId]; - } - _pendingReorderId = -1; - } else { - if (!newMetadata[idStr]) { - newMetadata[idStr] = { - timestamp: now, - appId: activeToplevel ? activeToplevel.appId : "", - appTitle: activeToplevel ? activeToplevel.title : "" - }; - } - } - _clipMetadata = newMetadata; - } - } - }; - }); - } - - Process { - id: clipProc - running: true - command: ["cliphist", "-preview-width", 9 ** 9, "-max-items", 9 ** 9, "list"] - stdout: StdioCollector { - property bool pending: false - property var onceFinished: null - - onStreamFinished: () => { - pending = true; - Qt.callLater(() => { - if (pending) { - pending = false; - _clipHist = this.text.split(/\r?\n/); - if (onceFinished) { - onceFinished(); - onceFinished = null; - } - } - }); - } - } - } - - Process { - id: clipDecodeProc - property var tmp: ({}) - property int index: 0 - property string separator: "\0\x01\x02CLIP_SEP_7b9a4c8d\x02\x01\0" - running: true - command: ["sh", "-c", 'cliphist list | while IFS= read -r line; do\n' + ' cliphist decode "$line"; ' + 'printf "\\0\\x01\\x02CLIP_SEP_7b9a4c8d\\x02\\x01\\0";\n' + 'done'] - onStarted: { - clipDecodeProc.tmp = []; - clipDecodeProc.index = 0; - } - onExited: { - if (clipDecodeProc.tmp) - root._clipDecoded = clipDecodeProc.tmp; - } - stdout: SplitParser { - onRead: data => { - const parts = data.split(clipDecodeProc.separator); - - if (!clipDecodeProc.tmp[clipDecodeProc.index]) - clipDecodeProc.tmp[clipDecodeProc.index] = ""; - - clipDecodeProc.tmp[clipDecodeProc.index] += parts[0]; - - for (let i = 1; i < parts.length; i++) { - clipDecodeProc.index++; - clipDecodeProc.tmp[clipDecodeProc.index] = parts[i]; - } - } - } - } - - Process { - id: imgProc - running: true - command: ["sh", Quickshell.shellDir + "/utils/cliphist-img.sh"] - stdout: StdioCollector { - property bool pending: false - onStreamFinished: () => { - pending = true; - Qt.callLater(() => { - if (pending) { - pending = false; - _clipImg = this.text.split(/\r?\n/); - } - }); - } - } - } - - Process { - id: watchProc - running: true - command: ["wl-paste", "-w", "echo"] - onRunningChanged: if (!running) - running = true - - stdout: SplitParser { - onRead: line => { - _captureMetadata(); - _update(); - } - } - } + id: root + + key: "data" + algorithm: General.defaultSearchAlgorithm + + property list<string> _clipHist: [] + property list<string> _clipImg: [] + property list<string> _clipImgIds: _clipImg.map(p => p.split("/").pop().split(".").shift()) + property var _clipMetadata: ({}) + property var _clipDecoded: ({}) + property int _pendingReorderId: -1 + + function getType(text: string): string { + if (!text) + return; + if (text.startsWith("http://") || text.startsWith("https://")) + return "web"; + if (text.startsWith("file://")) + return "file"; + + return "text"; + } + + list: { + if (!_clipHist?.length) + return []; + + const imgMap = _clipImgIds.slice(0, Math.min(_clipImg.length, _clipImgIds.length)).reduce((map, id, i) => { + if (id) + map[id] = _clipImg[i]; + return map; + }, {}); + + return _clipHist.filter(raw => raw).map((raw, idx) => { + const tab = raw.indexOf("\t"); + let idNum = -1; + let payload = raw; + + if (tab > 0) { + const n = parseInt(raw.slice(0, tab), 10); + if (!Number.isNaN(n)) { + idNum = n; + payload = raw.slice(tab + 1); + } + } + + const imgPath = idNum >= 0 ? imgMap[idNum] || "" : ""; + const isImg = !!imgPath; + const metadata = _clipMetadata[idNum] || {}; + + return { + index: idNum, + type: isImg ? "image" : root.getType(payload), + data: isImg ? imgPath : payload, + raw: raw, + timestamp: metadata.timestamp || "", + appId: metadata.appId || "", + appTitle: metadata.appTitle || "", + appIcon: Quickshell.iconPath(AppSearch.guessIcon(metadata.appId), "image-missing"), + image: imgPath, + decoded: root._clipDecoded[idx] || "" + }; + }); + } + + Persistent { + id: clipMetaStore + filePath: Directories.clipHistMetaDataPath + adapter: JsonAdapter { + property var metadata: ({}) + } + fileView.onLoaded: { + const meta = adapter.metadata; + for (const k in meta) { + if (meta[k].timestamp && typeof meta[k].timestamp === "string") { + meta[k].timestamp = new Date(meta[k].timestamp); + } + } + root._clipMetadata = meta; + } + } + + on_ClipMetadataChanged: { + clipMetaStore.adapter.metadata = _clipMetadata; + } + + function transformSearch(search) { + return search.toLowerCase(); + } + + function decodeAndCopy(text) { + const tab = text.indexOf("\t"); + if (tab > 0) { + const idStr = text.slice(0, tab); + const n = parseInt(idStr, 10); + if (!Number.isNaN(n)) + _pendingReorderId = n; + } + Quickshell.execDetached(["sh", "-c", `cliphist decode '${StringUtils.shellSingleQuoteEscape(text)}' | wl-copy`]); + } + + function copy(text) { + _pendingReorderId = -1; + Quickshell.execDetached(["wl-copy", text]); + } + + function _update() { + clipProc.running = true; + clipDecodeProc.running = true; + imgProc.running = true; + } + + function _captureMetadata() { + const now = new Date(); + const activeToplevel = ToplevelManager.activeToplevel; + + Qt.callLater(() => { + clipProc.running = true; + clipProc.stdout.onceFinished = () => { + const lines = clipProc.stdout.text.split(/\r?\n/); + const firstLine = lines[0] || ""; + const tab = firstLine.indexOf("\t"); + + if (tab > 0) { + const idStr = firstLine.slice(0, tab); + const n = parseInt(idStr, 10); + if (!Number.isNaN(n)) { + if (_clipMetadata[idStr]) { + _pendingReorderId = -1; + return; + } + + let newMetadata = {}; + for (var k in _clipMetadata) { + if (_clipMetadata.hasOwnProperty(k)) + newMetadata[k] = _clipMetadata[k]; + } + + if (_pendingReorderId >= 0) { + if (_clipMetadata[_pendingReorderId]) { + newMetadata[idStr] = _clipMetadata[_pendingReorderId]; + } + _pendingReorderId = -1; + } else { + if (!newMetadata[idStr]) { + newMetadata[idStr] = { + timestamp: now, + appId: activeToplevel ? activeToplevel.appId : "", + appTitle: activeToplevel ? activeToplevel.title : "" + }; + } + } + _clipMetadata = newMetadata; + } + } + }; + }); + } + + Process { + id: clipProc + running: true + command: ["cliphist", "-preview-width", 9 ** 9, "-max-items", 9 ** 9, "list"] + stdout: StdioCollector { + property bool pending: false + property var onceFinished: null + + onStreamFinished: () => { + pending = true; + Qt.callLater(() => { + if (pending) { + pending = false; + _clipHist = this.text.split(/\r?\n/); + if (onceFinished) { + onceFinished(); + onceFinished = null; + } + } + }); + } + } + } + + Process { + id: clipDecodeProc + property var tmp: ({}) + property int index: 0 + property string separator: "\0\x01\x02CLIP_SEP_7b9a4c8d\x02\x01\0" + running: true + command: ["sh", "-c", 'cliphist list | while IFS= read -r line; do\n' + ' cliphist decode "$line"; ' + 'printf "\\0\\x01\\x02CLIP_SEP_7b9a4c8d\\x02\\x01\\0";\n' + 'done'] + onStarted: { + clipDecodeProc.tmp = []; + clipDecodeProc.index = 0; + } + onExited: { + if (clipDecodeProc.tmp) + root._clipDecoded = clipDecodeProc.tmp; + } + stdout: SplitParser { + onRead: data => { + const parts = data.split(clipDecodeProc.separator); + + if (!clipDecodeProc.tmp[clipDecodeProc.index]) + clipDecodeProc.tmp[clipDecodeProc.index] = ""; + + clipDecodeProc.tmp[clipDecodeProc.index] += parts[0]; + + for (let i = 1; i < parts.length; i++) { + clipDecodeProc.index++; + clipDecodeProc.tmp[clipDecodeProc.index] = parts[i]; + } + } + } + } + + Process { + id: imgProc + running: true + command: ["sh", Quickshell.shellDir + "/utils/cliphist-img.sh"] + stdout: StdioCollector { + property bool pending: false + onStreamFinished: () => { + pending = true; + Qt.callLater(() => { + if (pending) { + pending = false; + _clipImg = this.text.split(/\r?\n/); + } + }); + } + } + } + + Process { + id: watchProc + running: true + command: ["wl-paste", "-w", "echo"] + onRunningChanged: if (!running) + running = true + + stdout: SplitParser { + onRead: line => { + _captureMetadata(); + _update(); + } + } + } } diff --git a/services/Emoji.qml b/services/Emoji.qml index 108fbd9..697840b 100644 --- a/services/Emoji.qml +++ b/services/Emoji.qml @@ -6,49 +6,49 @@ import QtQuick import qs.components Searchable { - id: root - - property var _emojis: [] - property var _emojiFile: FileView { - path: "/usr/share/rofi-emoji/all_emojis.txt" - onTextChanged: _parseEmojis() - } - - list: _emojis - keys: ["name", "keywords", "category", "subcategory", "emoji"] - algorithm: Searchable.SearchAlgorithm.Include - - function transformSearch(search) { - return search.toLowerCase(); - } - - function _parseEmojis() { - const text = _emojiFile.text(); - if (!text) { - console.warn("Emoji service: failed to load", _emojiFile.path); - _emojis = []; - return; - } - - const lines = text.split("\n").filter(l => l.trim()); - const out = []; - for (let i = 0; i < lines.length; ++i) { - const parts = lines[i].split("\t"); - if (parts.length < 5) - continue; - const [emoji, category, subcategory, name, keywords] = parts; - out.push({ - emoji: emoji, - category: category, - subcategory: subcategory, - name: name, - keywords: keywords - }); - } - _emojis = out; - } - - function copy(emoji) { - Clip.copy(emoji); - } + id: root + + property var _emojis: [] + property var _emojiFile: FileView { + path: "/usr/share/rofi-emoji/all_emojis.txt" + onTextChanged: _parseEmojis() + } + + list: _emojis + keys: ["name", "keywords", "category", "subcategory", "emoji"] + algorithm: Searchable.SearchAlgorithm.Include + + function transformSearch(search) { + return search.toLowerCase(); + } + + function _parseEmojis() { + const text = _emojiFile.text(); + if (!text) { + console.warn("Emoji service: failed to load", _emojiFile.path); + _emojis = []; + return; + } + + const lines = text.split("\n").filter(l => l.trim()); + const out = []; + for (let i = 0; i < lines.length; ++i) { + const parts = lines[i].split("\t"); + if (parts.length < 5) + continue; + const [emoji, category, subcategory, name, keywords] = parts; + out.push({ + emoji: emoji, + category: category, + subcategory: subcategory, + name: name, + keywords: keywords + }); + } + _emojis = out; + } + + function copy(emoji) { + Clip.copy(emoji); + } } diff --git a/services/Mpris.qml b/services/Mpris.qml index dc103f8..c73ed3f 100644 --- a/services/Mpris.qml +++ b/services/Mpris.qml @@ -16,154 +16,154 @@ import qs.config * A service that provides easy access to the active Mpris player. */ Singleton { - id: root - property MprisPlayer trackedPlayer: null - property MprisPlayer activePlayer: Mpris.players.values.find(i => i.desktopEntry === Bar.preferedPlayer) ?? trackedPlayer ?? Mpris.players.values[0] ?? null - signal trackChanged(reverse: bool) - - property bool __reverse: false - - property var activeTrack - - Timer { - running: root.activePlayer?.playbackState == MprisPlaybackState.Playing && root.activePlayer?.positionSupported - interval: 1000 - repeat: true - onTriggered: root.activePlayer.positionChanged() - } - - Instantiator { - model: Mpris.players - - Connections { - required property MprisPlayer modelData - target: modelData - - Component.onCompleted: { - if (root.trackedPlayer == null || modelData.isPlaying) { - root.trackedPlayer = modelData; - } - } - - Component.onDestruction: { - if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) { - for (const player of Mpris.players.values) { - if (player.playbackState.isPlaying) { - root.trackedPlayer = player; - break; - } - } - - if (trackedPlayer == null && Mpris.players.values.length != 0) { - trackedPlayer = Mpris.players.values[0]; - } - } - } - - function onPlaybackStateChanged() { - if (root.trackedPlayer !== modelData) - root.trackedPlayer = modelData; - } - } - } - - Connections { - target: activePlayer - - function onPostTrackChanged() { - root.updateTrack(); - } - - function onTrackArtUrlChanged() { - // console.log("arturl:", activePlayer.trackArtUrl) - // root.updateTrack(); - if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) { - // cantata likes to send cover updates *BEFORE* updating the track info. - // as such, art url changes shouldn't be able to break the reverse animation - const r = root.__reverse; - root.updateTrack(); - root.__reverse = r; - } - } - // not working https://github.com/quickshell-mirror/quickshell/issues/111 - // function onEntered() { - // } - } - - onActivePlayerChanged: this.updateTrack() - - function updateTrack() { - // console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`); - this.activeTrack = { - uniqueId: this.activePlayer?.uniqueId ?? 0, - artUrl: this.activePlayer?.trackArtUrl ?? "", - title: this.activePlayer?.trackTitle || qsTr("Unknown Title"), - artist: this.activePlayer?.trackArtist || qsTr("Unknown Artist"), - album: this.activePlayer?.trackAlbum || qsTr("Unknown Album"), - position: this.activePlayer?.position ?? 0, - length: this.activePlayer?.length ?? 0 - }; - - this.trackChanged(__reverse); - this.__reverse = false; - } - - function updateProgress() { - this.activePlayer.position; - } - - property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying - property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false - function togglePlaying() { - if (this.canTogglePlaying) - this.activePlayer.togglePlaying(); - } - - property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false - function previous() { - if (this.canGoPrevious) { - this.__reverse = true; - this.activePlayer.previous(); - } - } - - property bool canGoNext: this.activePlayer?.canGoNext ?? false - function next() { - if (this.canGoNext) { - this.__reverse = false; - this.activePlayer.next(); - } - } - - property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl - - property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl - property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None - function setLoopState(loopState: var) { - if (this.loopSupported) { - this.activePlayer.loopState = loopState; - } - } - - property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl - property bool hasShuffle: this.activePlayer?.shuffle ?? false - function setShuffle(shuffle: bool) { - if (this.shuffleSupported) { - this.activePlayer.shuffle = shuffle; - } - } - - function setActivePlayer(player: MprisPlayer) { - const targetPlayer = player ?? Mpris.players[0]; - console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`); - - if (targetPlayer && this.activePlayer) { - this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer); - } else { - // always animate forward if going to null - this.__reverse = false; - } - - this.trackedPlayer = targetPlayer; - } + id: root + property MprisPlayer trackedPlayer: null + property MprisPlayer activePlayer: Mpris.players.values.find(i => i.desktopEntry === Bar.preferedPlayer) ?? trackedPlayer ?? Mpris.players.values[0] ?? null + signal trackChanged(reverse: bool) + + property bool __reverse: false + + property var activeTrack + + Timer { + running: root.activePlayer?.playbackState == MprisPlaybackState.Playing && root.activePlayer?.positionSupported + interval: 1000 + repeat: true + onTriggered: root.activePlayer.positionChanged() + } + + Instantiator { + model: Mpris.players + + Connections { + required property MprisPlayer modelData + target: modelData + + Component.onCompleted: { + if (root.trackedPlayer == null || modelData.isPlaying) { + root.trackedPlayer = modelData; + } + } + + Component.onDestruction: { + if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) { + for (const player of Mpris.players.values) { + if (player.playbackState.isPlaying) { + root.trackedPlayer = player; + break; + } + } + + if (trackedPlayer == null && Mpris.players.values.length != 0) { + trackedPlayer = Mpris.players.values[0]; + } + } + } + + function onPlaybackStateChanged() { + if (root.trackedPlayer !== modelData) + root.trackedPlayer = modelData; + } + } + } + + Connections { + target: activePlayer + + function onPostTrackChanged() { + root.updateTrack(); + } + + function onTrackArtUrlChanged() { + // console.log("arturl:", activePlayer.trackArtUrl) + // root.updateTrack(); + if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) { + // cantata likes to send cover updates *BEFORE* updating the track info. + // as such, art url changes shouldn't be able to break the reverse animation + const r = root.__reverse; + root.updateTrack(); + root.__reverse = r; + } + } + // not working https://github.com/quickshell-mirror/quickshell/issues/111 + // function onEntered() { + // } + } + + onActivePlayerChanged: this.updateTrack() + + function updateTrack() { + // console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`); + this.activeTrack = { + uniqueId: this.activePlayer?.uniqueId ?? 0, + artUrl: this.activePlayer?.trackArtUrl ?? "", + title: this.activePlayer?.trackTitle || qsTr("Unknown Title"), + artist: this.activePlayer?.trackArtist || qsTr("Unknown Artist"), + album: this.activePlayer?.trackAlbum || qsTr("Unknown Album"), + position: this.activePlayer?.position ?? 0, + length: this.activePlayer?.length ?? 0 + }; + + this.trackChanged(__reverse); + this.__reverse = false; + } + + function updateProgress() { + this.activePlayer.position; + } + + property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying + property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false + function togglePlaying() { + if (this.canTogglePlaying) + this.activePlayer.togglePlaying(); + } + + property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false + function previous() { + if (this.canGoPrevious) { + this.__reverse = true; + this.activePlayer.previous(); + } + } + + property bool canGoNext: this.activePlayer?.canGoNext ?? false + function next() { + if (this.canGoNext) { + this.__reverse = false; + this.activePlayer.next(); + } + } + + property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl + + property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl + property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None + function setLoopState(loopState: var) { + if (this.loopSupported) { + this.activePlayer.loopState = loopState; + } + } + + property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl + property bool hasShuffle: this.activePlayer?.shuffle ?? false + function setShuffle(shuffle: bool) { + if (this.shuffleSupported) { + this.activePlayer.shuffle = shuffle; + } + } + + function setActivePlayer(player: MprisPlayer) { + const targetPlayer = player ?? Mpris.players[0]; + console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`); + + if (targetPlayer && this.activePlayer) { + this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer); + } else { + // always animate forward if going to null + this.__reverse = false; + } + + this.trackedPlayer = targetPlayer; + } } diff --git a/services/Network.qml b/services/Network.qml index 231841a..bfe6c12 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -9,78 +9,78 @@ import QtQuick import qs.config Singleton { - id: root + id: root - property bool wifi: true - property bool ethernet: false - property string networkName: "" - property int networkStrength - property string state: ethernet ? Icons.resource.network.wifi : (networkName.length >= 0 && networkName !== "lo") ? (networkStrength >= 90 ? Icons.resource.network.wifi.max : networkStrength >= 80 ? Icons.resource.network.wifi.high : networkStrength >= 60 ? Icons.resource.network.wifi.mid : networkStrength >= 40 ? Icons.resource.network.wifi.low : networkStrength >= 20 ? Icons.resource.network.wifi.min : Icons.resource.network.disconnected) : Icons.resource.network.disconnected + property bool wifi: true + property bool ethernet: false + property string networkName: "" + property int networkStrength + property string state: ethernet ? Icons.resource.network.wifi : (networkName.length >= 0 && networkName !== "lo") ? (networkStrength >= 90 ? Icons.resource.network.wifi.max : networkStrength >= 80 ? Icons.resource.network.wifi.high : networkStrength >= 60 ? Icons.resource.network.wifi.mid : networkStrength >= 40 ? Icons.resource.network.wifi.low : networkStrength >= 20 ? Icons.resource.network.wifi.min : Icons.resource.network.disconnected) : Icons.resource.network.disconnected - function update() { - updateConnectionType.startCheck(); - updateNetworkName.running = true; - updateNetworkStrength.running = true; - } + function update() { + updateConnectionType.startCheck(); + updateNetworkName.running = true; + updateNetworkStrength.running = true; + } - Timer { - interval: General.resourceUpdateInterval - running: true - repeat: true - onTriggered: { - root.update(); - interval = General.resourceUpdateInterval * 1000; - } - } + Timer { + interval: General.resourceUpdateInterval + running: true + repeat: true + onTriggered: { + root.update(); + interval = General.resourceUpdateInterval * 1000; + } + } - Process { - id: updateConnectionType - property string buffer - command: ["nmcli", "-t", "-f", "NAME,TYPE,DEVICE", "c", "show", "--active"] - running: true - function startCheck() { - buffer = ""; - updateConnectionType.running = true; - } - stdout: SplitParser { - onRead: data => { - updateConnectionType.buffer += data + "\n"; - } - } - onExited: (exitCode, exitStatus) => { - const lines = updateConnectionType.buffer.trim().split('\n'); - let hasEthernet = false; - let hasWifi = false; - lines.forEach(line => { - if (line.includes("ethernet")) - hasEthernet = true; - else if (line.includes("wireless")) - hasWifi = true; - }); - root.ethernet = hasEthernet; - root.wifi = hasWifi; - } - } + Process { + id: updateConnectionType + property string buffer + command: ["nmcli", "-t", "-f", "NAME,TYPE,DEVICE", "c", "show", "--active"] + running: true + function startCheck() { + buffer = ""; + updateConnectionType.running = true; + } + stdout: SplitParser { + onRead: data => { + updateConnectionType.buffer += data + "\n"; + } + } + onExited: (exitCode, exitStatus) => { + const lines = updateConnectionType.buffer.trim().split('\n'); + let hasEthernet = false; + let hasWifi = false; + lines.forEach(line => { + if (line.includes("ethernet")) + hasEthernet = true; + else if (line.includes("wireless")) + hasWifi = true; + }); + root.ethernet = hasEthernet; + root.wifi = hasWifi; + } + } - Process { - id: updateNetworkName - command: ["sh", "-c", "nmcli -t -f NAME c show --active | head -1"] - running: true - stdout: SplitParser { - onRead: data => { - root.networkName = data; - } - } - } + Process { + id: updateNetworkName + command: ["sh", "-c", "nmcli -t -f NAME c show --active | head -1"] + running: true + stdout: SplitParser { + onRead: data => { + root.networkName = data; + } + } + } - Process { - id: updateNetworkStrength - running: true - command: ["sh", "-c", "nmcli -f IN-USE,SIGNAL,SSID device wifi | awk '/^\*/{if (NR!=1) {print $2}}'"] - stdout: SplitParser { - onRead: data => { - root.networkStrength = parseInt(data); - } - } - } + Process { + id: updateNetworkStrength + running: true + command: ["sh", "-c", "nmcli -f IN-USE,SIGNAL,SSID device wifi | awk '/^\*/{if (NR!=1) {print $2}}'"] + stdout: SplitParser { + onRead: data => { + root.networkStrength = parseInt(data); + } + } + } } diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index c503891..9179e52 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -8,65 +8,65 @@ import "../utils/networkUsage.js" as Network import qs.config Singleton { - property int up: 0 - property string upUnit: "" - property int down: 0 - property string downUnit: "" + property int up: 0 + property string upUnit: "" + property int down: 0 + property string downUnit: "" - property string __iface: "" - property var __prev: ({ - rx: 0, - tx: 0 - }) + property string __iface: "" + property var __prev: ({ + rx: 0, + tx: 0 + }) - Timer { - interval: General.resourceUpdateInterval * 1000 - running: true - repeat: true - onTriggered: update() - } + Timer { + interval: General.resourceUpdateInterval * 1000 + running: true + repeat: true + onTriggered: update() + } - function update() { - try { - __iface = Network.defaultInterface(getRouteProc.stdout.text); - } catch (e) { - getRouteProc.running = true; - } - const cur = Network.getRxTxBytes(getNetDevProc.stdout.text, __iface); - const sec = General.resourceUpdateInterval; - const rawUp = (cur.tx - __prev.tx) / sec; - const rawDown = (cur.rx - __prev.rx) / sec; + function update() { + try { + __iface = Network.defaultInterface(getRouteProc.stdout.text); + } catch (e) { + getRouteProc.running = true; + } + const cur = Network.getRxTxBytes(getNetDevProc.stdout.text, __iface); + const sec = General.resourceUpdateInterval; + const rawUp = (cur.tx - __prev.tx) / sec; + const rawDown = (cur.rx - __prev.rx) / sec; - up = Network.formatSize(rawUp, false); - upUnit = Network.formatSize(rawUp, true); - down = Network.formatSize(rawDown, false); - downUnit = Network.formatSize(rawDown, true); + up = Network.formatSize(rawUp, false); + upUnit = Network.formatSize(rawUp, true); + down = Network.formatSize(rawDown, false); + downUnit = Network.formatSize(rawDown, true); - __prev = cur; - getRouteProc.running = true; - getNetDevProc.running = true; - } + __prev = cur; + getRouteProc.running = true; + getNetDevProc.running = true; + } - Process { - id: getRouteProc - running: true - command: ["ip", "route"] - stdout: StdioCollector {} - } + Process { + id: getRouteProc + running: true + command: ["ip", "route"] + stdout: StdioCollector {} + } - Process { - id: getNetDevProc - running: true - command: ["cat", "/proc/net/dev"] - stdout: StdioCollector {} - } + Process { + id: getNetDevProc + running: true + command: ["cat", "/proc/net/dev"] + stdout: StdioCollector {} + } - Component.onCompleted: { - getRouteProc.running = true; - getNetDevProc.running = true; - if (getRouteProc.stdout.text) - __iface = Network.defaultInterface(getRouteProc.stdout.text); - if (getNetDevProc.stdout.text) - __prev = Network.getRxTxBytes(getNetDevProc.stdout.text, __iface); - } + Component.onCompleted: { + getRouteProc.running = true; + getNetDevProc.running = true; + if (getRouteProc.stdout.text) + __iface = Network.defaultInterface(getRouteProc.stdout.text); + if (getNetDevProc.stdout.text) + __prev = Network.getRxTxBytes(getNetDevProc.stdout.text, __iface); + } } diff --git a/services/Persistent.qml b/services/Persistent.qml index 3a911bf..9077ce9 100644 --- a/services/Persistent.qml +++ b/services/Persistent.qml @@ -4,24 +4,24 @@ import Quickshell.Io import qs.utils Scope { - id: root - property alias adapter: fileView.adapter - property alias fileView: fileView - property string fileDir: Directories.state - property string fileName: "states.json" - property string filePath: `${root.fileDir}/${root.fileName}` + id: root + property alias adapter: fileView.adapter + property alias fileView: fileView + property string fileDir: Directories.state + property string fileName: "states.json" + property string filePath: `${root.fileDir}/${root.fileName}` - FileView { - id: fileView - path: root.filePath - watchChanges: true - onFileChanged: reload() - onAdapterUpdated: writeAdapter() - onLoadFailed: error => { - console.log("Failed to load persistent states file:", error); - if (error == FileViewError.FileNotFound) { - writeAdapter(); - } - } - } + FileView { + id: fileView + path: root.filePath + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + onLoadFailed: error => { + console.log("Failed to load persistent states file:", error); + if (error == FileViewError.FileNotFound) { + writeAdapter(); + } + } + } } diff --git a/services/Resource.qml b/services/Resource.qml index 3c6c6ea..e94d8c0 100644 --- a/services/Resource.qml +++ b/services/Resource.qml @@ -7,114 +7,114 @@ import Quickshell.Io import qs.config Singleton { - id: root + id: root - property double memoryTotal: 1 - property double memoryFree: 1 - property double memoryUsed: memoryTotal - memoryFree - property double memoryUsedPercentage: memoryUsed / memoryTotal - property double swapTotal: 1 - property double swapFree: 1 - property double swapUsed: swapTotal - swapFree - property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 + property double memoryTotal: 1 + property double memoryFree: 1 + property double memoryUsed: memoryTotal - memoryFree + property double memoryUsedPercentage: memoryUsed / memoryTotal + property double swapTotal: 1 + property double swapFree: 1 + property double swapUsed: swapTotal - swapFree + property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 - property double cpuUsage: 0 - property double cpuTemp: 0 - property double cpuFreq: 0 - property var previousCpuStats + property double cpuUsage: 0 + property double cpuTemp: 0 + property double cpuFreq: 0 + property var previousCpuStats - property int gpuUsage: 0 - property int gpuTemp: 0 + property int gpuUsage: 0 + property int gpuTemp: 0 - Timer { - interval: General.resourceUpdateInterval - running: true - repeat: true - onTriggered: { - fileMeminfo.reload(); - fileStat.reload(); - fileCpuThermal.reload(); - fileCpuInfo.reload(); + Timer { + interval: General.resourceUpdateInterval + running: true + repeat: true + onTriggered: { + fileMeminfo.reload(); + fileStat.reload(); + fileCpuThermal.reload(); + fileCpuInfo.reload(); - const textMeminfo = fileMeminfo.text(); - memoryTotal = Number(textMeminfo.match(/MemTotal:\s*(\d+)/)[1] ?? 1); - memoryFree = Number(textMeminfo.match(/MemAvailable:\s*(\d+)/)[1] ?? 0); - memoryUsed = memoryTotal - memoryFree; - memoryUsedPercentage = memoryUsed / memoryTotal; - swapTotal = Number(textMeminfo.match(/SwapTotal:\s*(\d+)/)[1] ?? 1); - swapFree = Number(textMeminfo.match(/SwapFree:\s*(\d+)/)[1] ?? 0); + const textMeminfo = fileMeminfo.text(); + memoryTotal = Number(textMeminfo.match(/MemTotal:\s*(\d+)/)[1] ?? 1); + memoryFree = Number(textMeminfo.match(/MemAvailable:\s*(\d+)/)[1] ?? 0); + memoryUsed = memoryTotal - memoryFree; + memoryUsedPercentage = memoryUsed / memoryTotal; + swapTotal = Number(textMeminfo.match(/SwapTotal:\s*(\d+)/)[1] ?? 1); + swapFree = Number(textMeminfo.match(/SwapFree:\s*(\d+)/)[1] ?? 0); - const textStat = fileStat.text(); - const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); - if (cpuLine) { - const stats = cpuLine.slice(1).map(Number); - const total = stats.reduce((a, b) => a + b, 0); - const idle = stats[3]; - if (root.previousCpuStats) { - const td = total - root.previousCpuStats.total; - const id = idle - root.previousCpuStats.idle; - cpuUsage = td > 0 ? (1 - id / td) : 0; - } - root.previousCpuStats = { - total, - idle - }; - } + const textStat = fileStat.text(); + const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); + if (cpuLine) { + const stats = cpuLine.slice(1).map(Number); + const total = stats.reduce((a, b) => a + b, 0); + const idle = stats[3]; + if (root.previousCpuStats) { + const td = total - root.previousCpuStats.total; + const id = idle - root.previousCpuStats.idle; + cpuUsage = td > 0 ? (1 - id / td) : 0; + } + root.previousCpuStats = { + total, + idle + }; + } - const cpuTempText = fileCpuThermal.text(); - cpuTemp = cpuTempText ? parseInt(cpuTempText.trim()) / 100000 : 0; + const cpuTempText = fileCpuThermal.text(); + cpuTemp = cpuTempText ? parseInt(cpuTempText.trim()) / 100000 : 0; - const textCpuInfo = fileCpuInfo.text(); - let sum = 0, cnt = 0; - const re = /^cpu MHz\s*:\s*([\d.]+)/mg; - let m; - while ((m = re.exec(textCpuInfo)) !== null) { - sum += Number(m[1]); - cnt++; - } - cpuFreq = cnt ? sum / cnt : 0; + const textCpuInfo = fileCpuInfo.text(); + let sum = 0, cnt = 0; + const re = /^cpu MHz\s*:\s*([\d.]+)/mg; + let m; + while ((m = re.exec(textCpuInfo)) !== null) { + sum += Number(m[1]); + cnt++; + } + cpuFreq = cnt ? sum / cnt : 0; - gpuUsageProc.running = true; - gpuTempProc.running = true; - interval = General.resourceUpdateInterval * cpuUsage > 0.7 || memoryUsedPercentage > 0.7 || cpuTemp > 0.7 ? 100 : 1000; - } - } + gpuUsageProc.running = true; + gpuTempProc.running = true; + interval = General.resourceUpdateInterval * cpuUsage > 0.7 || memoryUsedPercentage > 0.7 || cpuTemp > 0.7 ? 100 : 1000; + } + } - FileView { - id: fileMeminfo - path: "/proc/meminfo" - } - FileView { - id: fileStat - path: "/proc/stat" - } - FileView { - id: fileCpuThermal - path: General.cpuThermalPath - } - FileView { - id: fileCpuInfo - path: "/proc/cpuinfo" - } + FileView { + id: fileMeminfo + path: "/proc/meminfo" + } + FileView { + id: fileStat + path: "/proc/stat" + } + FileView { + id: fileCpuThermal + path: General.cpuThermalPath + } + FileView { + id: fileCpuInfo + path: "/proc/cpuinfo" + } - Process { - id: gpuUsageProc - running: true - command: ["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"] - stdout: StdioCollector { - onStreamFinished: { - root.gpuUsage = parseInt(this.text.trim()); - } - } - } - Process { - id: gpuTempProc - running: true - command: ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"] - stdout: StdioCollector { - onStreamFinished: { - root.gpuTemp = parseInt(this.text.trim()); - } - } - } + Process { + id: gpuUsageProc + running: true + command: ["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"] + stdout: StdioCollector { + onStreamFinished: { + root.gpuUsage = parseInt(this.text.trim()); + } + } + } + Process { + id: gpuTempProc + running: true + command: ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"] + stdout: StdioCollector { + onStreamFinished: { + root.gpuTemp = parseInt(this.text.trim()); + } + } + } } diff --git a/services/TimeDate.qml b/services/TimeDate.qml index f066f46..ebefbc1 100644 --- a/services/TimeDate.qml +++ b/services/TimeDate.qml @@ -4,79 +4,79 @@ import Quickshell import QtQuick Singleton { - id: root - readonly property string time: { - // fuck you qt give me my 12 hr time - var h = clock.date.getHours(); - var m = clock.date.getMinutes(); - var s = clock.date.getSeconds(); - h = h % 12; - if (h === 0) - h = 12; - return (h < 10 ? "0" : "") + h + ":" + (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s; - } - readonly property int hours: { - var h = clock.date.getHours(); - h = h % 12; - if (h === 0) - h = 12; - return h; - } - readonly property int minutes: clock.date.getMinutes() - readonly property int seconds: clock.date.getSeconds() + id: root + readonly property string time: { + // fuck you qt give me my 12 hr time + var h = clock.date.getHours(); + var m = clock.date.getMinutes(); + var s = clock.date.getSeconds(); + h = h % 12; + if (h === 0) + h = 12; + return (h < 10 ? "0" : "") + h + ":" + (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s; + } + readonly property int hours: { + var h = clock.date.getHours(); + h = h % 12; + if (h === 0) + h = 12; + return h; + } + readonly property int minutes: clock.date.getMinutes() + readonly property int seconds: clock.date.getSeconds() - readonly property string date: Qt.formatDateTime(clock.date, "dddd dd/MM") + readonly property string date: Qt.formatDateTime(clock.date, "dddd dd/MM") - function sinceWhen(d) { - if (typeof d === "string") - return; + function sinceWhen(d) { + if (typeof d === "string") + return; - var now = clock.date; + var now = clock.date; - function startOfDay(x) { - return new Date(x.getFullYear(), x.getMonth(), x.getDate(), 0, 0, 0, 0); - } + function startOfDay(x) { + return new Date(x.getFullYear(), x.getMonth(), x.getDate(), 0, 0, 0, 0); + } - function startOfWeek(x) { - var dow = x.getDay(); - var mondayOffset = (dow + 6) % 7; - var sod = startOfDay(x); - sod.setDate(sod.getDate() - mondayOffset); - return sod; - } + function startOfWeek(x) { + var dow = x.getDay(); + var mondayOffset = (dow + 6) % 7; + var sod = startOfDay(x); + sod.setDate(sod.getDate() - mondayOffset); + return sod; + } - function fmt12h(dateObj) { - var h24 = dateObj.getHours(); - var m = dateObj.getMinutes(); - var h12 = h24 % 12; - if (h12 === 0) - h12 = 12; - var ampm = h24 < 12 ? "AM" : "PM"; - return ((h12 < 10 ? "0" : "") + h12 + ":" + (m < 10 ? "0" : "") + m + " " + ampm); - } + function fmt12h(dateObj) { + var h24 = dateObj.getHours(); + var m = dateObj.getMinutes(); + var h12 = h24 % 12; + if (h12 === 0) + h12 = 12; + var ampm = h24 < 12 ? "AM" : "PM"; + return ((h12 < 10 ? "0" : "") + h12 + ":" + (m < 10 ? "0" : "") + m + " " + ampm); + } - function weekdayName(dateObj) { - return Qt.formatDateTime(dateObj, "dddd"); - } + function weekdayName(dateObj) { + return Qt.formatDateTime(dateObj, "dddd"); + } - var nowSOD = startOfDay(now).getTime(); - var dSOD = startOfDay(d).getTime(); - if (nowSOD === dSOD) { - return fmt12h(d); - } + var nowSOD = startOfDay(now).getTime(); + var dSOD = startOfDay(d).getTime(); + if (nowSOD === dSOD) { + return fmt12h(d); + } - var nowSOW = startOfWeek(now).getTime(); - var dSOW = startOfWeek(d).getTime(); - if (nowSOW === dSOW) { - return weekdayName(d) + " " + fmt12h(d); - } + var nowSOW = startOfWeek(now).getTime(); + var dSOW = startOfWeek(d).getTime(); + if (nowSOW === dSOW) { + return weekdayName(d) + " " + fmt12h(d); + } - var ymd = Qt.formatDateTime(d, "yyyy-MM-dd"); - return ymd + " at " + fmt12h(d); - } + var ymd = Qt.formatDateTime(d, "yyyy-MM-dd"); + return ymd + " at " + fmt12h(d); + } - SystemClock { - id: clock - precision: SystemClock.Seconds - } + SystemClock { + id: clock + precision: SystemClock.Seconds + } } @@ -12,49 +12,49 @@ import qs.services // TODO: remove all MouseArea and use modern alternative Scope { - id: root + id: root - property var standaloneObj: ({}) + property var standaloneObj: ({}) - Bar {} + Bar {} - PersistentProperties { - id: loaderProp - reloadableId: "launcherLoaderProp" + PersistentProperties { + id: loaderProp + reloadableId: "launcherLoaderProp" - property bool active: false - } - // TODO: cache/don't destory when closed(active=false) - LazyLoader { - id: launcherLoader + property bool active: false + } + // TODO: cache/don't destory when closed(active=false) + LazyLoader { + id: launcherLoader - active: loaderProp.active + active: loaderProp.active - component: Launcher { - parentLoader: loaderProp - } - } + component: Launcher { + parentLoader: loaderProp + } + } - IpcHandler { - target: "qs" + IpcHandler { + target: "qs" - function reload(hard: bool): void { - Quickshell.reload(hard); - } - } - IpcHandler { - target: "launcher" + function reload(hard: bool): void { + Quickshell.reload(hard); + } + } + IpcHandler { + target: "launcher" - function toggle() { - loaderProp.active = !loaderProp.active; - } + function toggle() { + loaderProp.active = !loaderProp.active; + } - function standalone(component: string): void { - if (root.standaloneObj.name === component) { - root.standaloneObj.obj.active = !root.standaloneObj.obj.active; - return; - } - root.standaloneObj.obj = Qt.createQmlObject(` + function standalone(component: string): void { + if (root.standaloneObj.name === component) { + root.standaloneObj.obj.active = !root.standaloneObj.obj.active; + return; + } + root.standaloneObj.obj = Qt.createQmlObject(` import Quickshell import QtQuick import "popups/launcher" @@ -93,17 +93,17 @@ Scope { } } `, root); - root.standaloneObj.name = component; - } - } - - Connections { - target: Quickshell - function onReloadCompleted() { - Quickshell.inhibitReloadPopup(); - } - // function onReloadFailed() { - // Quickshell.inhibitReloadPopup(); - // } - } + root.standaloneObj.name = component; + } + } + + Connections { + target: Quickshell + function onReloadCompleted() { + Quickshell.inhibitReloadPopup(); + } + // function onReloadFailed() { + // Quickshell.inhibitReloadPopup(); + // } + } } |
