Merge branch 'noctalia-dev:main' into fix/heuristic-lookup
This commit is contained in:
commit
81182aa65b
56 changed files with 2680 additions and 1646 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -116,9 +117,7 @@ Singleton {
|
||||||
id: adapter
|
id: adapter
|
||||||
|
|
||||||
// bar
|
// bar
|
||||||
property JsonObject bar
|
property JsonObject bar: JsonObject {
|
||||||
|
|
||||||
bar: JsonObject {
|
|
||||||
property string position: "top" // Possible values: "top", "bottom"
|
property string position: "top" // Possible values: "top", "bottom"
|
||||||
property bool showActiveWindowIcon: true
|
property bool showActiveWindowIcon: true
|
||||||
property bool alwaysShowBatteryPercentage: false
|
property bool alwaysShowBatteryPercentage: false
|
||||||
|
|
@ -130,14 +129,12 @@ Singleton {
|
||||||
widgets: JsonObject {
|
widgets: JsonObject {
|
||||||
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
|
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
|
||||||
property list<string> center: ["Workspace"]
|
property list<string> center: ["Workspace"]
|
||||||
property list<string> right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
|
property list<string> right: ["ScreenRecorderIndicator", "Tray", "ArchUpdater", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// general
|
// general
|
||||||
property JsonObject general
|
property JsonObject general: JsonObject {
|
||||||
|
|
||||||
general: JsonObject {
|
|
||||||
property string avatarImage: defaultAvatar
|
property string avatarImage: defaultAvatar
|
||||||
property bool dimDesktop: false
|
property bool dimDesktop: false
|
||||||
property bool showScreenCorners: false
|
property bool showScreenCorners: false
|
||||||
|
|
@ -145,9 +142,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// location
|
// location
|
||||||
property JsonObject location
|
property JsonObject location: JsonObject {
|
||||||
|
|
||||||
location: JsonObject {
|
|
||||||
property string name: "Tokyo"
|
property string name: "Tokyo"
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool reverseDayMonth: false
|
property bool reverseDayMonth: false
|
||||||
|
|
@ -156,9 +151,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// screen recorder
|
// screen recorder
|
||||||
property JsonObject screenRecorder
|
property JsonObject screenRecorder: JsonObject {
|
||||||
|
|
||||||
screenRecorder: JsonObject {
|
|
||||||
property string directory: "~/Videos"
|
property string directory: "~/Videos"
|
||||||
property int frameRate: 60
|
property int frameRate: 60
|
||||||
property string audioCodec: "opus"
|
property string audioCodec: "opus"
|
||||||
|
|
@ -171,9 +164,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wallpaper
|
// wallpaper
|
||||||
property JsonObject wallpaper
|
property JsonObject wallpaper: JsonObject {
|
||||||
|
|
||||||
wallpaper: JsonObject {
|
|
||||||
property string directory: "/usr/share/wallpapers"
|
property string directory: "/usr/share/wallpapers"
|
||||||
property string current: ""
|
property string current: ""
|
||||||
property bool isRandom: false
|
property bool isRandom: false
|
||||||
|
|
@ -194,9 +185,7 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// applauncher
|
// applauncher
|
||||||
property JsonObject appLauncher
|
property JsonObject appLauncher: JsonObject {
|
||||||
|
|
||||||
appLauncher: JsonObject {
|
|
||||||
// When disabled, Launcher hides clipboard command and ignores cliphist
|
// When disabled, Launcher hides clipboard command and ignores cliphist
|
||||||
property bool enableClipboardHistory: true
|
property bool enableClipboardHistory: true
|
||||||
// Position: center, top_left, top_right, bottom_left, bottom_right
|
// Position: center, top_left, top_right, bottom_left, bottom_right
|
||||||
|
|
@ -205,43 +194,34 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// dock
|
// dock
|
||||||
property JsonObject dock
|
property JsonObject dock: JsonObject {
|
||||||
|
|
||||||
dock: JsonObject {
|
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
property bool exclusive: false
|
property bool exclusive: false
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// network
|
// network
|
||||||
property JsonObject network
|
property JsonObject network: JsonObject {
|
||||||
|
|
||||||
network: JsonObject {
|
|
||||||
property bool wifiEnabled: true
|
property bool wifiEnabled: true
|
||||||
property bool bluetoothEnabled: true
|
property bool bluetoothEnabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
property JsonObject notifications
|
property JsonObject notifications: JsonObject {
|
||||||
|
|
||||||
notifications: JsonObject {
|
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
property JsonObject audio
|
property JsonObject audio: JsonObject {
|
||||||
|
|
||||||
audio: JsonObject {
|
|
||||||
property bool showMiniplayerAlbumArt: false
|
property bool showMiniplayerAlbumArt: false
|
||||||
property bool showMiniplayerCava: false
|
property bool showMiniplayerCava: false
|
||||||
property string visualizerType: "linear"
|
property string visualizerType: "linear"
|
||||||
property int volumeStep: 5
|
property int volumeStep: 5
|
||||||
|
property int cavaFrameRate: 60
|
||||||
}
|
}
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
property JsonObject ui
|
property JsonObject ui: JsonObject {
|
||||||
|
|
||||||
ui: JsonObject {
|
|
||||||
property string fontDefault: "Roboto" // Default font for all text
|
property string fontDefault: "Roboto" // Default font for all text
|
||||||
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
|
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
|
||||||
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
|
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
|
||||||
|
|
@ -259,15 +239,11 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
// brightness
|
// brightness
|
||||||
property JsonObject brightness
|
property JsonObject brightness: JsonObject {
|
||||||
|
|
||||||
brightness: JsonObject {
|
|
||||||
property int brightnessStep: 5
|
property int brightnessStep: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject colorSchemes
|
property JsonObject colorSchemes: JsonObject {
|
||||||
|
|
||||||
colorSchemes: JsonObject {
|
|
||||||
property bool useWallpaperColors: false
|
property bool useWallpaperColors: false
|
||||||
property string predefinedScheme: ""
|
property string predefinedScheme: ""
|
||||||
property bool darkMode: true
|
property bool darkMode: true
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Commons
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// Signal emitted when widget loading status changes
|
|
||||||
signal widgetLoaded(string widgetName)
|
|
||||||
signal widgetFailed(string widgetName, string error)
|
|
||||||
signal loadingComplete(int total, int loaded, int failed)
|
|
||||||
|
|
||||||
// Properties to track loading status
|
|
||||||
property int totalWidgets: 0
|
|
||||||
property int loadedWidgets: 0
|
|
||||||
property int failedWidgets: 0
|
|
||||||
|
|
||||||
// Auto-discover widget components
|
|
||||||
function getWidgetComponent(widgetName) {
|
|
||||||
if (!widgetName || widgetName.trim() === "") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const widgetPath = `../Modules/Bar/Widgets/${widgetName}.qml`
|
|
||||||
|
|
||||||
// Try to load the widget directly from file
|
|
||||||
const component = Qt.createComponent(widgetPath)
|
|
||||||
if (component.status === Component.Ready) {
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMsg = `Failed to load ${widgetName}.qml widget, status: ${component.status}, error: ${component.errorString(
|
|
||||||
)}`
|
|
||||||
Logger.error("WidgetLoader", errorMsg)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize loading tracking
|
|
||||||
function initializeLoading(widgetList) {
|
|
||||||
totalWidgets = widgetList.length
|
|
||||||
loadedWidgets = 0
|
|
||||||
failedWidgets = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track widget loading success
|
|
||||||
function onWidgetLoaded(widgetName) {
|
|
||||||
loadedWidgets++
|
|
||||||
widgetLoaded(widgetName)
|
|
||||||
|
|
||||||
if (loadedWidgets + failedWidgets === totalWidgets) {
|
|
||||||
Logger.log("WidgetLoader", `Loaded ${loadedWidgets} widgets`)
|
|
||||||
loadingComplete(totalWidgets, loadedWidgets, failedWidgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track widget loading failure
|
|
||||||
function onWidgetFailed(widgetName, error) {
|
|
||||||
failedWidgets++
|
|
||||||
widgetFailed(widgetName, error)
|
|
||||||
|
|
||||||
if (loadedWidgets + failedWidgets === totalWidgets) {
|
|
||||||
loadingComplete(totalWidgets, loadedWidgets, failedWidgets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is where you should add your Modules/Bar/Widgets/
|
|
||||||
// so it gets registered in the BarTab
|
|
||||||
function discoverAvailableWidgets() {
|
|
||||||
const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "MediaMini", "NotificationHistory", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"]
|
|
||||||
|
|
||||||
const availableWidgets = []
|
|
||||||
|
|
||||||
widgetFiles.forEach(widgetName => {
|
|
||||||
// Test if the widget can be loaded
|
|
||||||
const component = getWidgetComponent(widgetName)
|
|
||||||
if (component) {
|
|
||||||
availableWidgets.push({
|
|
||||||
"key": widgetName,
|
|
||||||
"name": widgetName
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sort alphabetically
|
|
||||||
availableWidgets.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
|
|
||||||
return availableWidgets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
230
Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml
Normal file
230
Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NPanel {
|
||||||
|
id: root
|
||||||
|
panelWidth: 380 * scaling
|
||||||
|
panelHeight: 500 * scaling
|
||||||
|
panelAnchorRight: true
|
||||||
|
|
||||||
|
// Auto-refresh when service updates
|
||||||
|
Connections {
|
||||||
|
target: ArchUpdaterService
|
||||||
|
function onUpdatePackagesChanged() {
|
||||||
|
// Force UI update when packages change
|
||||||
|
if (root.visible) {
|
||||||
|
// Small delay to ensure data is fully updated
|
||||||
|
Qt.callLater(() => {
|
||||||
|
// Force a UI update by triggering a property change
|
||||||
|
ArchUpdaterService.updatePackages = ArchUpdaterService.updatePackages
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panelContent: Rectangle {
|
||||||
|
color: Color.mSurface
|
||||||
|
radius: Style.radiusL * scaling
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginL * scaling
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
text: "system_update"
|
||||||
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "System Updates"
|
||||||
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
tooltipText: "Close"
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update summary
|
||||||
|
Text {
|
||||||
|
text: ArchUpdaterService.updatePackages.length + " package" + (ArchUpdaterService.updatePackages.length
|
||||||
|
!== 1 ? "s" : "") + " can be updated"
|
||||||
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package selection info
|
||||||
|
Text {
|
||||||
|
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.updatePackages.length + " packages selected"
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package list
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
radius: Style.radiusM * scaling
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: packageListView
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginS * scaling
|
||||||
|
clip: true
|
||||||
|
model: ArchUpdaterService.updatePackages
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: packageListView.width
|
||||||
|
height: 50 * scaling
|
||||||
|
color: Color.transparent
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginS * scaling
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
|
// Checkbox for selection
|
||||||
|
NIconButton {
|
||||||
|
id: checkbox
|
||||||
|
icon: "check_box_outline_blank"
|
||||||
|
onClicked: {
|
||||||
|
const isSelected = ArchUpdaterService.isPackageSelected(modelData.name)
|
||||||
|
if (isSelected) {
|
||||||
|
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||||
|
icon = "check_box_outline_blank"
|
||||||
|
colorFg = Color.mOnSurfaceVariant
|
||||||
|
} else {
|
||||||
|
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||||
|
icon = "check_box"
|
||||||
|
colorFg = Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colorBg: Color.transparent
|
||||||
|
colorFg: Color.mOnSurfaceVariant
|
||||||
|
Layout.preferredWidth: 30 * scaling
|
||||||
|
Layout.preferredHeight: 30 * scaling
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Set initial state
|
||||||
|
if (ArchUpdaterService.isPackageSelected(modelData.name)) {
|
||||||
|
icon = "check_box"
|
||||||
|
colorFg = Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package info
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginXXS * scaling
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.oldVersion + " → " + modelData.newVersion
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "refresh"
|
||||||
|
tooltipText: "Check for updates"
|
||||||
|
onClicked: {
|
||||||
|
ArchUpdaterService.doPoll()
|
||||||
|
}
|
||||||
|
colorBg: Color.mSurfaceVariant
|
||||||
|
colorFg: Color.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 35 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update"
|
||||||
|
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
|
||||||
|
enabled: !ArchUpdaterService.updateInProgress
|
||||||
|
onClicked: {
|
||||||
|
ArchUpdaterService.runUpdate()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : Color.mPrimary
|
||||||
|
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : Color.mOnPrimary
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 35 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings"
|
||||||
|
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
|
||||||
|
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
|
||||||
|
onClicked: {
|
||||||
|
if (ArchUpdaterService.selectedPackagesCount > 0) {
|
||||||
|
ArchUpdaterService.runSelectiveUpdate()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||||
|
> 0 ? Color.mSecondary : Color.mSurfaceVariant)
|
||||||
|
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||||
|
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant)
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 35 * scaling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.UPower
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
@ -47,6 +48,7 @@ Variants {
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
// Left Section - Dynamic Widgets
|
// Left Section - Dynamic Widgets
|
||||||
Row {
|
Row {
|
||||||
id: leftSection
|
id: leftSection
|
||||||
|
|
@ -60,21 +62,19 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Settings.data.bar.widgets.left
|
model: Settings.data.bar.widgets.left
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: leftWidgetLoader
|
|
||||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
|
||||||
active: true
|
active: true
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
sourceComponent: NWidgetLoader {
|
||||||
onStatusChanged: {
|
widgetName: modelData
|
||||||
if (status === Loader.Error) {
|
widgetProps: {
|
||||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
"screen": screen
|
||||||
} else if (status === Loader.Ready) {
|
|
||||||
widgetLoader.onWidgetLoaded(modelData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
// Center Section - Dynamic Widgets
|
// Center Section - Dynamic Widgets
|
||||||
Row {
|
Row {
|
||||||
id: centerSection
|
id: centerSection
|
||||||
|
|
@ -87,21 +87,19 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Settings.data.bar.widgets.center
|
model: Settings.data.bar.widgets.center
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: centerWidgetLoader
|
|
||||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
|
||||||
active: true
|
active: true
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
sourceComponent: NWidgetLoader {
|
||||||
onStatusChanged: {
|
widgetName: modelData
|
||||||
if (status === Loader.Error) {
|
widgetProps: {
|
||||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
"screen": screen
|
||||||
} else if (status === Loader.Ready) {
|
|
||||||
widgetLoader.onWidgetLoaded(modelData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
// Right Section - Dynamic Widgets
|
// Right Section - Dynamic Widgets
|
||||||
Row {
|
Row {
|
||||||
id: rightSection
|
id: rightSection
|
||||||
|
|
@ -115,35 +113,17 @@ Variants {
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Settings.data.bar.widgets.right
|
model: Settings.data.bar.widgets.right
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: rightWidgetLoader
|
|
||||||
sourceComponent: widgetLoader.getWidgetComponent(modelData)
|
|
||||||
active: true
|
active: true
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
sourceComponent: NWidgetLoader {
|
||||||
onStatusChanged: {
|
widgetName: modelData
|
||||||
if (status === Loader.Error) {
|
widgetProps: {
|
||||||
widgetLoader.onWidgetFailed(modelData, "Loader error")
|
"screen": screen
|
||||||
} else if (status === Loader.Ready) {
|
|
||||||
widgetLoader.onWidgetLoaded(modelData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget loader instance
|
|
||||||
WidgetLoader {
|
|
||||||
id: widgetLoader
|
|
||||||
|
|
||||||
onWidgetFailed: function (widgetName, error) {
|
|
||||||
Logger.error("Bar", `Widget failed: ${widgetName} - ${error}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize widget loading tracking
|
|
||||||
Component.onCompleted: {
|
|
||||||
const allWidgets = [...Settings.data.bar.widgets.left, ...Settings.data.bar.widgets.center, ...Settings.data.bar.widgets.right]
|
|
||||||
widgetLoader.initializeLoading(allWidgets)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,20 @@ import qs.Widgets
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
property bool showingFullTitle: false
|
||||||
|
property int lastWindowIndex: -1
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
visible: getTitle() !== ""
|
visible: getTitle() !== ""
|
||||||
|
|
||||||
property bool showingFullTitle: false
|
|
||||||
property int lastWindowIndex: -1
|
|
||||||
|
|
||||||
// Timer to hide full title after window switch
|
// Timer to hide full title after window switch
|
||||||
Timer {
|
Timer {
|
||||||
id: fullTitleTimer
|
id: fullTitleTimer
|
||||||
interval: Style.animationSlow * 4 // Show full title for 2 seconds
|
interval: 2000
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
showingFullTitle = false
|
showingFullTitle = false
|
||||||
|
|
@ -40,8 +43,9 @@ Row {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTitle() {
|
function getTitle() {
|
||||||
const focusedWindow = CompositorService.getFocusedWindow()
|
// Use the service's focusedWindowTitle property which is updated immediately
|
||||||
return focusedWindow ? (focusedWindow.title || focusedWindow.appId || "") : ""
|
// when WindowOpenedOrChanged events are received
|
||||||
|
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppIcon() {
|
function getAppIcon() {
|
||||||
|
|
@ -62,6 +66,7 @@ Row {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
// Let the Rectangle size itself based on its content (the Row)
|
// Let the Rectangle size itself based on its content (the Row)
|
||||||
|
visible: root.visible
|
||||||
width: row.width + Style.marginM * scaling * 2
|
width: row.width + Style.marginM * scaling * 2
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
height: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
|
|
@ -100,10 +105,10 @@ Row {
|
||||||
NText {
|
NText {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
|
||||||
// If hovered or just switched window, show up to 300 pixels
|
// If hovered or just switched window, show up to 400 pixels
|
||||||
// If not hovered show up to 150 pixels
|
// If not hovered show up to 150 pixels
|
||||||
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
width: (showingFullTitle || mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||||
300 * scaling) : Math.min(
|
400 * scaling) : Math.min(
|
||||||
fullTitleMetrics.contentWidth, 150 * scaling)
|
fullTitleMetrics.contentWidth, 150 * scaling)
|
||||||
text: getTitle()
|
text: getTitle()
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
|
|
||||||
75
Modules/Bar/Widgets/ArchUpdater.qml
Normal file
75
Modules/Bar/Widgets/ArchUpdater.qml
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
colorBg: Color.mSurfaceVariant
|
||||||
|
colorFg: Color.mOnSurface
|
||||||
|
colorBorder: Color.transparent
|
||||||
|
colorBorderHover: Color.transparent
|
||||||
|
|
||||||
|
// Enhanced icon states with better visual feedback
|
||||||
|
icon: {
|
||||||
|
if (ArchUpdaterService.busy)
|
||||||
|
return "sync"
|
||||||
|
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||||
|
// Show different icons based on update count
|
||||||
|
const count = ArchUpdaterService.updatePackages.length
|
||||||
|
if (count > 50)
|
||||||
|
return "system_update_alt" // Many updates
|
||||||
|
if (count > 10)
|
||||||
|
return "system_update" // Moderate updates
|
||||||
|
return "system_update" // Few updates
|
||||||
|
}
|
||||||
|
return "task_alt"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced tooltip with more information
|
||||||
|
tooltipText: {
|
||||||
|
if (ArchUpdaterService.busy)
|
||||||
|
return "Checking for updates…"
|
||||||
|
|
||||||
|
var count = ArchUpdaterService.updatePackages.length
|
||||||
|
if (count === 0)
|
||||||
|
return "System is up to date ✓"
|
||||||
|
|
||||||
|
var header = count === 1 ? "One package can be upgraded:" : (count + " packages can be upgraded:")
|
||||||
|
|
||||||
|
var list = ArchUpdaterService.updatePackages || []
|
||||||
|
var s = ""
|
||||||
|
var limit = Math.min(list.length, 8)
|
||||||
|
// Reduced to 8 for better readability
|
||||||
|
for (var i = 0; i < limit; ++i) {
|
||||||
|
var p = list[i]
|
||||||
|
s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion)
|
||||||
|
}
|
||||||
|
if (list.length > 8)
|
||||||
|
s += "\n… and " + (list.length - 8) + " more"
|
||||||
|
|
||||||
|
return header + "\n\n" + s + "\n\nClick to update system"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced click behavior with confirmation
|
||||||
|
onClicked: {
|
||||||
|
if (ArchUpdaterService.busy)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (ArchUpdaterService.updatePackages.length > 0) {
|
||||||
|
// Show confirmation dialog for updates
|
||||||
|
PanelService.getPanel("archUpdaterPanel").toggle(screen)
|
||||||
|
} else {
|
||||||
|
// Just refresh if no updates available
|
||||||
|
ArchUpdaterService.doPoll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,89 +6,100 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
NPill {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Test mode
|
property ShellScreen screen
|
||||||
property bool testMode: false
|
property real scaling: ScalingService.scale(screen)
|
||||||
property int testPercent: 49
|
|
||||||
property bool testCharging: false
|
|
||||||
|
|
||||||
property var battery: UPower.displayDevice
|
implicitWidth: pill.width
|
||||||
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
implicitHeight: pill.height
|
||||||
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
|
||||||
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
|
||||||
property bool show: isReady && percent > 0
|
|
||||||
|
|
||||||
// Choose icon based on charge and charging state
|
NPill {
|
||||||
function batteryIcon() {
|
id: pill
|
||||||
|
|
||||||
if (charging)
|
// Test mode
|
||||||
return "battery_android_bolt"
|
property bool testMode: false
|
||||||
|
property int testPercent: 49
|
||||||
|
property bool testCharging: false
|
||||||
|
|
||||||
if (percent >= 95)
|
property var battery: UPower.displayDevice
|
||||||
return "battery_android_full"
|
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
|
||||||
|
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
|
||||||
|
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
|
||||||
|
|
||||||
// Hardcoded battery symbols
|
// Choose icon based on charge and charging state
|
||||||
if (percent >= 85)
|
function batteryIcon() {
|
||||||
return "battery_android_6"
|
|
||||||
if (percent >= 70)
|
|
||||||
return "battery_android_5"
|
|
||||||
if (percent >= 55)
|
|
||||||
return "battery_android_4"
|
|
||||||
if (percent >= 40)
|
|
||||||
return "battery_android_3"
|
|
||||||
if (percent >= 25)
|
|
||||||
return "battery_android_2"
|
|
||||||
if (percent >= 10)
|
|
||||||
return "battery_android_1"
|
|
||||||
if (percent >= 0)
|
|
||||||
return "battery_android_0"
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: testMode || (isReady && battery.isLaptopBattery)
|
if (!isReady || !battery.isLaptopBattery)
|
||||||
|
return "battery_android_alert"
|
||||||
|
|
||||||
icon: root.batteryIcon()
|
if (charging)
|
||||||
text: Math.round(root.percent) + "%"
|
return "battery_android_bolt"
|
||||||
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
|
||||||
forceShown: Settings.data.bar.alwaysShowBatteryPercentage
|
|
||||||
tooltipText: {
|
|
||||||
let lines = []
|
|
||||||
|
|
||||||
if (testMode) {
|
if (percent >= 95)
|
||||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
|
return "battery_android_full"
|
||||||
|
|
||||||
|
// Hardcoded battery symbols
|
||||||
|
if (percent >= 85)
|
||||||
|
return "battery_android_6"
|
||||||
|
if (percent >= 70)
|
||||||
|
return "battery_android_5"
|
||||||
|
if (percent >= 55)
|
||||||
|
return "battery_android_4"
|
||||||
|
if (percent >= 40)
|
||||||
|
return "battery_android_3"
|
||||||
|
if (percent >= 25)
|
||||||
|
return "battery_android_2"
|
||||||
|
if (percent >= 10)
|
||||||
|
return "battery_android_1"
|
||||||
|
if (percent >= 0)
|
||||||
|
return "battery_android_0"
|
||||||
|
}
|
||||||
|
|
||||||
|
icon: batteryIcon()
|
||||||
|
text: (isReady && battery.isLaptopBattery) ? Math.round(percent) + "%" : "-"
|
||||||
|
textColor: charging ? Color.mPrimary : Color.mOnSurface
|
||||||
|
forceOpen: isReady && battery.isLaptopBattery && Settings.data.bar.alwaysShowBatteryPercentage
|
||||||
|
disableOpen: (!isReady || !battery.isLaptopBattery)
|
||||||
|
tooltipText: {
|
||||||
|
let lines = []
|
||||||
|
|
||||||
|
if (testMode) {
|
||||||
|
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(12345))
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isReady || !battery.isLaptopBattery) {
|
||||||
|
return "No Battery Detected"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery.timeToEmpty > 0) {
|
||||||
|
lines.push("Time Left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery.timeToFull > 0) {
|
||||||
|
lines.push("Time Until Full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery.changeRate !== undefined) {
|
||||||
|
const rate = battery.changeRate
|
||||||
|
if (rate > 0) {
|
||||||
|
lines.push(charging ? "Charging Rate: " + rate.toFixed(2) + " W" : "Discharging Rate: " + rate.toFixed(
|
||||||
|
2) + " W")
|
||||||
|
} else if (rate < 0) {
|
||||||
|
lines.push("Discharging Rate: " + Math.abs(rate).toFixed(2) + " W")
|
||||||
|
} else {
|
||||||
|
lines.push("Estimating...")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(charging ? "Charging" : "Discharging")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
|
||||||
|
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")
|
||||||
|
}
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.isReady) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.battery.timeToEmpty > 0) {
|
|
||||||
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(root.battery.timeToEmpty))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.battery.timeToFull > 0) {
|
|
||||||
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(root.battery.timeToFull))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.battery.changeRate !== undefined) {
|
|
||||||
const rate = root.battery.changeRate
|
|
||||||
if (rate > 0) {
|
|
||||||
lines.push(root.charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
|
|
||||||
2) + " W")
|
|
||||||
} else if (rate < 0) {
|
|
||||||
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W")
|
|
||||||
} else {
|
|
||||||
lines.push("Estimating...")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lines.push(root.charging ? "Charging" : "Discharging")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.battery.healthPercentage !== undefined && root.battery.healthPercentage > 0) {
|
|
||||||
lines.push("Health: " + Math.round(root.battery.healthPercentage) + "%")
|
|
||||||
}
|
|
||||||
return lines.join("\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,11 @@ import qs.Widgets
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
sizeMultiplier: 0.8
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
visible: Settings.data.network.bluetoothEnabled
|
||||||
|
sizeMultiplier: 0.8
|
||||||
colorBg: Color.mSurfaceVariant
|
colorBg: Color.mSurfaceVariant
|
||||||
colorFg: Color.mOnSurface
|
colorFg: Color.mOnSurface
|
||||||
colorBorder: Color.transparent
|
colorBorder: Color.transparent
|
||||||
|
|
@ -28,7 +31,5 @@ NIconButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltipText: "Bluetooth Devices"
|
tooltipText: "Bluetooth Devices"
|
||||||
onClicked: {
|
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen)
|
||||||
bluetoothPanel.toggle(screen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,16 @@ import qs.Widgets
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
width: pill.width
|
property ShellScreen screen
|
||||||
height: pill.height
|
property real scaling: ScalingService.scale(screen)
|
||||||
visible: getMonitor() !== null
|
|
||||||
|
|
||||||
// Used to avoid opening the pill on Quickshell startup
|
// Used to avoid opening the pill on Quickshell startup
|
||||||
property bool firstBrightnessReceived: false
|
property bool firstBrightnessReceived: false
|
||||||
|
|
||||||
|
implicitWidth: pill.width
|
||||||
|
implicitHeight: pill.height
|
||||||
|
visible: getMonitor() !== null
|
||||||
|
|
||||||
function getMonitor() {
|
function getMonitor() {
|
||||||
return BrightnessService.getMonitorForScreen(screen) || null
|
return BrightnessService.getMonitorForScreen(screen) || null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
// Clock Icon with attached calendar
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
width: clock.width + Style.marginM * 2 * scaling
|
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
implicitWidth: clock.width + Style.marginM * 2 * scaling
|
||||||
|
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
// Clock Icon with attached calendar
|
||||||
NClock {
|
NClock {
|
||||||
id: clock
|
id: clock
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -24,7 +29,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (!calendarPanel.active) {
|
if (!PanelService.getPanel("calendarPanel")?.active) {
|
||||||
tooltip.show()
|
tooltip.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +38,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
tooltip.hide()
|
tooltip.hide()
|
||||||
calendarPanel.toggle(screen)
|
PanelService.getPanel("calendarPanel")?.toggle(screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
Modules/Bar/Widgets/KeyboardLayout.qml
Normal file
36
Modules/Bar/Widgets/KeyboardLayout.qml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
// Use the shared service for keyboard layout
|
||||||
|
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||||
|
|
||||||
|
width: pill.width
|
||||||
|
height: pill.height
|
||||||
|
|
||||||
|
NPill {
|
||||||
|
id: pill
|
||||||
|
icon: "keyboard_alt"
|
||||||
|
iconCircleColor: Color.mPrimary
|
||||||
|
collapsedIconColor: Color.mOnSurface
|
||||||
|
autoHide: false // Important to be false so we can hover as long as we want
|
||||||
|
text: currentLayout
|
||||||
|
tooltipText: "Keyboard Layout: " + currentLayout
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
|
||||||
|
// You could open keyboard settings here if needed
|
||||||
|
// For now, just show the current layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,10 +9,14 @@ import qs.Widgets
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
visible: MediaService.currentPlayer !== null
|
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||||
width: MediaService.currentPlayer !== null ? implicitWidth : 0
|
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
||||||
|
|
||||||
function getTitle() {
|
function getTitle() {
|
||||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||||
|
|
@ -144,7 +148,7 @@ Row {
|
||||||
NText {
|
NText {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
|
||||||
// If hovered or just switched window, show up to 300 pixels
|
// If hovered or just switched window, show up to 400 pixels
|
||||||
// If not hovered show up to 150 pixels
|
// If not hovered show up to 150 pixels
|
||||||
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
width: (mouseArea.containsMouse) ? Math.min(fullTitleMetrics.contentWidth,
|
||||||
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
|
400 * scaling) : Math.min(fullTitleMetrics.contentWidth,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ import qs.Widgets
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
icon: "notifications"
|
icon: "notifications"
|
||||||
tooltipText: "Notification History"
|
tooltipText: "Notification History"
|
||||||
|
|
@ -17,8 +20,5 @@ NIconButton {
|
||||||
colorFg: Color.mOnSurface
|
colorFg: Color.mOnSurface
|
||||||
colorBorder: Color.transparent
|
colorBorder: Color.transparent
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen)
|
||||||
onClicked: {
|
|
||||||
notificationHistoryPanel.toggle(screen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
Modules/Bar/Widgets/PowerProfile.qml
Normal file
60
Modules/Bar/Widgets/PowerProfile.qml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.UPower
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
property var powerProfiles: PowerProfiles
|
||||||
|
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
|
||||||
|
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
visible: hasPP
|
||||||
|
|
||||||
|
function profileIcon() {
|
||||||
|
if (!hasPP)
|
||||||
|
return "balance"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Performance)
|
||||||
|
return "speed"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||||
|
return "balance"
|
||||||
|
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||||
|
return "eco"
|
||||||
|
}
|
||||||
|
|
||||||
|
function profileName() {
|
||||||
|
if (!hasPP)
|
||||||
|
return "Unknown"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Performance)
|
||||||
|
return "Performance"
|
||||||
|
if (powerProfiles.profile === PowerProfile.Balanced)
|
||||||
|
return "Balanced"
|
||||||
|
if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||||
|
return "Power Saver"
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeProfile() {
|
||||||
|
if (!hasPP)
|
||||||
|
return
|
||||||
|
if (powerProfiles.profile === PowerProfile.Performance)
|
||||||
|
powerProfiles.profile = PowerProfile.PowerSaver
|
||||||
|
else if (powerProfiles.profile === PowerProfile.Balanced)
|
||||||
|
powerProfiles.profile = PowerProfile.Performance
|
||||||
|
else if (powerProfiles.profile === PowerProfile.PowerSaver)
|
||||||
|
powerProfiles.profile = PowerProfile.Balanced
|
||||||
|
}
|
||||||
|
|
||||||
|
icon: root.profileIcon()
|
||||||
|
tooltipText: root.profileName()
|
||||||
|
colorBg: Color.mSurfaceVariant
|
||||||
|
colorFg: Color.mOnSurface
|
||||||
|
colorBorder: Color.transparent
|
||||||
|
colorBorderHover: Color.transparent
|
||||||
|
onClicked: root.changeProfile()
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
// Screen Recording Indicator
|
// Screen Recording Indicator
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: screenRecordingIndicator
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
visible: ScreenRecorderService.isRecording
|
||||||
icon: "videocam"
|
icon: "videocam"
|
||||||
tooltipText: "Screen Recording Active\nClick To Stop Recording"
|
tooltipText: "Screen Recording Active\nClick To Stop Recording"
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
colorBg: Color.mPrimary
|
colorBg: Color.mPrimary
|
||||||
colorFg: Color.mOnPrimary
|
colorFg: Color.mOnPrimary
|
||||||
visible: ScreenRecorderService.isRecording
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: {
|
onClicked: ScreenRecorderService.toggleRecording()
|
||||||
ScreenRecorderService.toggleRecording()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: sidePanelToggle
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
icon: "widgets"
|
icon: "widgets"
|
||||||
tooltipText: "Open Side Panel"
|
tooltipText: "Open Side Panel"
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
|
|
@ -14,5 +19,5 @@ NIconButton {
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: sidePanel.toggle(screen)
|
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import qs.Widgets
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,20 @@ import Quickshell
|
||||||
import Quickshell.Services.SystemTray
|
import Quickshell.Services.SystemTray
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
|
import qs.Modules.Bar.Extras
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
readonly property real itemSize: 24 * scaling
|
readonly property real itemSize: 24 * scaling
|
||||||
|
|
||||||
visible: SystemTray.items.values.length > 0
|
visible: SystemTray.items.values.length > 0
|
||||||
width: tray.width + Style.marginM * scaling * 2
|
implicitWidth: tray.width + Style.marginM * scaling * 2
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
|
@ -134,9 +139,7 @@ Rectangle {
|
||||||
function open() {
|
function open() {
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
// Register into the panel service
|
PanelService.willOpenPanel(trayPanel)
|
||||||
// so this will autoclose if we open another panel
|
|
||||||
PanelService.registerOpen(trayPanel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
|
@ -152,7 +155,7 @@ Rectangle {
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: trayMenu
|
id: trayMenu
|
||||||
source: "TrayMenu.qml"
|
source: "../Extras/TrayMenu.qml"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,15 @@ import qs.Widgets
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
width: pill.width
|
property ShellScreen screen
|
||||||
height: pill.height
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
// Used to avoid opening the pill on Quickshell startup
|
// Used to avoid opening the pill on Quickshell startup
|
||||||
property bool firstVolumeReceived: false
|
property bool firstVolumeReceived: false
|
||||||
|
|
||||||
|
implicitWidth: pill.width
|
||||||
|
implicitHeight: pill.height
|
||||||
|
|
||||||
function getIcon() {
|
function getIcon() {
|
||||||
if (AudioService.muted) {
|
if (AudioService.muted) {
|
||||||
return "volume_off"
|
return "volume_off"
|
||||||
|
|
@ -64,6 +67,7 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
||||||
settingsPanel.open(screen)
|
settingsPanel.open(screen)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import qs.Widgets
|
||||||
NIconButton {
|
NIconButton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
|
visible: Settings.data.network.wifiEnabled
|
||||||
|
|
||||||
sizeMultiplier: 0.8
|
sizeMultiplier: 0.8
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|
@ -27,6 +32,8 @@ NIconButton {
|
||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
try {
|
try {
|
||||||
|
if (NetworkService.ethernet)
|
||||||
|
return "lan"
|
||||||
let connected = false
|
let connected = false
|
||||||
let signalStrength = 0
|
let signalStrength = 0
|
||||||
for (const net in NetworkService.networks) {
|
for (const net in NetworkService.networks) {
|
||||||
|
|
@ -36,17 +43,17 @@ NIconButton {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return connected ? NetworkService.signalIcon(signalStrength) : "wifi"
|
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error("WiFi", "Error getting icon:", error)
|
Logger.error("WiFi", "Error getting icon:", error)
|
||||||
return "wifi"
|
return "signal_wifi_bad"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltipText: "WiFi Networks"
|
tooltipText: "Network / WiFi"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
try {
|
try {
|
||||||
Logger.log("WiFi", "Button clicked, toggling panel")
|
Logger.log("WiFi", "Button clicked, toggling panel")
|
||||||
wifiPanel.toggle(screen)
|
PanelService.getPanel("wifiPanel")?.toggle(screen)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error("WiFi", "Error toggling panel:", error)
|
Logger.error("WiFi", "Error toggling panel:", error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ import qs.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property ShellScreen screen: null
|
||||||
|
property real scaling: ScalingService.scale(screen)
|
||||||
|
|
||||||
property bool isDestroying: false
|
property bool isDestroying: false
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
|
|
||||||
|
|
@ -23,7 +27,8 @@ Item {
|
||||||
|
|
||||||
signal workspaceChanged(int workspaceId, color accentColor)
|
signal workspaceChanged(int workspaceId, color accentColor)
|
||||||
|
|
||||||
width: {
|
implicitHeight: Math.round(36 * scaling)
|
||||||
|
implicitWidth: {
|
||||||
let total = 0
|
let total = 0
|
||||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||||
const ws = localWorkspaces.get(i)
|
const ws = localWorkspaces.get(i)
|
||||||
|
|
@ -39,34 +44,35 @@ Item {
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
height: Math.round(36 * scaling)
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
localWorkspaces.clear()
|
refreshWorkspaces()
|
||||||
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
|
||||||
const ws = WorkspaceService.workspaces.get(i)
|
|
||||||
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
|
||||||
localWorkspaces.append(ws)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workspaceRepeater.model = localWorkspaces
|
|
||||||
updateWorkspaceFocus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
root.isDestroying = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onScreenChanged: refreshWorkspaces()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: WorkspaceService
|
target: WorkspaceService
|
||||||
function onWorkspacesChanged() {
|
function onWorkspacesChanged() {
|
||||||
localWorkspaces.clear()
|
refreshWorkspaces()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshWorkspaces() {
|
||||||
|
localWorkspaces.clear()
|
||||||
|
if (screen !== null) {
|
||||||
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
|
||||||
const ws = WorkspaceService.workspaces.get(i)
|
const ws = WorkspaceService.workspaces.get(i)
|
||||||
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
|
||||||
localWorkspaces.append(ws)
|
localWorkspaces.append(ws)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaceRepeater.model = localWorkspaces
|
|
||||||
updateWorkspaceFocus()
|
|
||||||
}
|
}
|
||||||
|
workspaceRepeater.model = localWorkspaces
|
||||||
|
updateWorkspaceFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerUnifiedWave() {
|
function triggerUnifiedWave() {
|
||||||
|
|
@ -74,6 +80,17 @@ Item {
|
||||||
masterAnimation.restart()
|
masterAnimation.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateWorkspaceFocus() {
|
||||||
|
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||||
|
const ws = localWorkspaces.get(i)
|
||||||
|
if (ws.isFocused === true) {
|
||||||
|
root.triggerUnifiedWave()
|
||||||
|
root.workspaceChanged(ws.id, Color.mPrimary)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
id: masterAnimation
|
id: masterAnimation
|
||||||
PropertyAction {
|
PropertyAction {
|
||||||
|
|
@ -101,17 +118,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWorkspaceFocus() {
|
|
||||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
|
||||||
const ws = localWorkspaces.get(i)
|
|
||||||
if (ws.isFocused === true) {
|
|
||||||
root.triggerUnifiedWave()
|
|
||||||
root.workspaceChanged(ws.id, Color.mPrimary)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: workspaceBackground
|
id: workspaceBackground
|
||||||
width: parent.width - Style.marginS * scaling * 2
|
width: parent.width - Style.marginS * scaling * 2
|
||||||
|
|
@ -254,8 +260,4 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
root.isDestroying = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,17 +104,6 @@ NPanel {
|
||||||
year: Time.date.getFullYear()
|
year: Time.date.getFullYear()
|
||||||
locale: Qt.locale() // Use system locale
|
locale: Qt.locale() // Use system locale
|
||||||
|
|
||||||
// Optionally, update when the panel becomes visible
|
|
||||||
Connections {
|
|
||||||
target: calendarPanel
|
|
||||||
function onVisibleChanged() {
|
|
||||||
if (calendarPanel.visible) {
|
|
||||||
grid.month = Time.date.getMonth()
|
|
||||||
grid.year = Time.date.getFullYear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: (Style.baseWidgetSize * scaling)
|
width: (Style.baseWidgetSize * scaling)
|
||||||
height: (Style.baseWidgetSize * scaling)
|
height: (Style.baseWidgetSize * scaling)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ Item {
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "settings"
|
target: "settings"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
settingsPanel.toggle(Quickshell.screens[0])
|
settingsPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
|
@ -16,43 +15,64 @@ Item {
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "notifications"
|
target: "notifications"
|
||||||
|
|
||||||
function toggleHistory() {
|
function toggleHistory() {
|
||||||
notificationHistoryPanel.toggle(Quickshell.screens[0])
|
notificationHistoryPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDoNotDisturb() {// TODO
|
function toggleDoNotDisturb() {// TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "idleInhibitor"
|
target: "idleInhibitor"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
return IdleInhibitorService.manualToggle()
|
return IdleInhibitorService.manualToggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For backward compatibility, should be removed soon(tmc)
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "appLauncher"
|
target: "appLauncher"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
launcherPanel.toggle(Quickshell.screens[0])
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
function clipboard() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set clipboard mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">clip ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function calculator() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set calculator mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">calc ")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "launcher"
|
target: "launcher"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
launcherPanel.toggle(Quickshell.screens[0])
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
}
|
}
|
||||||
|
function clipboard() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set clipboard mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">clip ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function calculator() {
|
||||||
|
launcherPanel.toggle(Quickshell.screens[0])
|
||||||
|
// Use the setSearchText function to set calculator mode
|
||||||
|
Qt.callLater(() => {
|
||||||
|
launcherPanel.setSearchText(">calc ")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "lockScreen"
|
target: "lockScreen"
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
// Only lock if not already locked (prevents the red screen issue)
|
// Only lock if not already locked (prevents the red screen issue)
|
||||||
// Note: No unlock via IPC for security reasons
|
// Note: No unlock via IPC for security reasons
|
||||||
|
|
@ -64,13 +84,25 @@ Item {
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "brightness"
|
target: "brightness"
|
||||||
|
|
||||||
function increase() {
|
function increase() {
|
||||||
BrightnessService.increaseBrightness()
|
BrightnessService.increaseBrightness()
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrease() {
|
function decrease() {
|
||||||
BrightnessService.decreaseBrightness()
|
BrightnessService.decreaseBrightness()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "powerPanel"
|
||||||
|
function toggle() {
|
||||||
|
powerPanel.toggle(Quickshell.screens[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "sidePanel"
|
||||||
|
function toggle() {
|
||||||
|
sidePanel.toggle(Quickshell.screens[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ QtObject {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to basic evaluation
|
// Fallback to basic evaluation
|
||||||
console.log("AdvancedMath not available, using basic eval")
|
Logger.warn("Calculator", "AdvancedMath not available, using basic eval")
|
||||||
|
|
||||||
// Basic preprocessing for common functions
|
// Basic preprocessing for common functions
|
||||||
var processed = expression.trim(
|
var processed = expression.trim(
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,30 @@ NPanel {
|
||||||
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
|
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
|
||||||
panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right"))
|
panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right"))
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
property string searchText: ""
|
||||||
|
property bool shouldResetCursor: false
|
||||||
|
|
||||||
|
// Add function to set search text programmatically
|
||||||
|
function setSearchText(text) {
|
||||||
|
searchText = text
|
||||||
|
// The searchInput will automatically update via the text binding
|
||||||
|
// Focus and cursor position will be handled by the TextField's Component.onCompleted
|
||||||
|
}
|
||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
// Reset state when panel opens to avoid sticky modes
|
// Reset state when panel opens to avoid sticky modes
|
||||||
|
if (searchText === "") {
|
||||||
|
searchText = ""
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
// Reset search bar when launcher is closed
|
||||||
searchText = ""
|
searchText = ""
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
|
shouldResetCursor = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import modular components
|
// Import modular components
|
||||||
|
|
@ -50,7 +70,6 @@ NPanel {
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
property var desktopEntries: DesktopEntries.applications.values
|
property var desktopEntries: DesktopEntries.applications.values
|
||||||
property string searchText: ""
|
|
||||||
property int selectedIndex: 0
|
property int selectedIndex: 0
|
||||||
|
|
||||||
// Refresh clipboard when user starts typing clipboard commands
|
// Refresh clipboard when user starts typing clipboard commands
|
||||||
|
|
@ -141,15 +160,11 @@ NPanel {
|
||||||
|
|
||||||
// Command execution functions
|
// Command execution functions
|
||||||
function executeCalcCommand() {
|
function executeCalcCommand() {
|
||||||
searchText = ">calc "
|
setSearchText(">calc ")
|
||||||
searchInput.text = searchText
|
|
||||||
searchInput.cursorPosition = searchText.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeClipCommand() {
|
function executeClipCommand() {
|
||||||
searchText = ">clip "
|
setSearchText(">clip ")
|
||||||
searchInput.text = searchText
|
|
||||||
searchInput.cursorPosition = searchText.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation functions
|
// Navigation functions
|
||||||
|
|
@ -252,10 +267,20 @@ NPanel {
|
||||||
anchors.leftMargin: Style.marginS * scaling
|
anchors.leftMargin: Style.marginS * scaling
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: searchText
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
searchText = text
|
// Update the parent searchText property
|
||||||
|
if (searchText !== text) {
|
||||||
|
searchText = text
|
||||||
|
}
|
||||||
// Defer selectedIndex reset to avoid binding loops
|
// Defer selectedIndex reset to avoid binding loops
|
||||||
Qt.callLater(() => selectedIndex = 0)
|
Qt.callLater(() => selectedIndex = 0)
|
||||||
|
|
||||||
|
// Reset cursor position if needed
|
||||||
|
if (shouldResetCursor && text === "") {
|
||||||
|
cursorPosition = 0
|
||||||
|
shouldResetCursor = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
selectedTextColor: Color.mOnSurface
|
selectedTextColor: Color.mOnSurface
|
||||||
selectionColor: Color.mPrimary
|
selectionColor: Color.mPrimary
|
||||||
|
|
@ -266,10 +291,14 @@ NPanel {
|
||||||
topPadding: 0
|
topPadding: 0
|
||||||
bottomPadding: 0
|
bottomPadding: 0
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Focus the search bar by default
|
// Focus the search bar by default and set cursor position
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
searchInput.forceActiveFocus()
|
searchInput.forceActiveFocus()
|
||||||
|
// Set cursor to end if there's already text
|
||||||
|
if (searchText && searchText.length > 0) {
|
||||||
|
searchInput.cursorPosition = searchText.length
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Keys.onDownPressed: selectNext()
|
Keys.onDownPressed: selectNext()
|
||||||
|
|
|
||||||
94
Modules/LockScreen/LockContext.qml
Normal file
94
Modules/LockScreen/LockContext.qml
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pam
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
signal unlocked
|
||||||
|
signal failed
|
||||||
|
|
||||||
|
property string currentText: ""
|
||||||
|
property bool unlockInProgress: false
|
||||||
|
property bool showFailure: false
|
||||||
|
property string errorMessage: ""
|
||||||
|
property bool pamAvailable: typeof PamContext !== "undefined"
|
||||||
|
|
||||||
|
onCurrentTextChanged: {
|
||||||
|
if (currentText !== "") {
|
||||||
|
showFailure = false
|
||||||
|
errorMessage = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryUnlock() {
|
||||||
|
if (!pamAvailable) {
|
||||||
|
errorMessage = "PAM not available"
|
||||||
|
showFailure = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentText === "") {
|
||||||
|
errorMessage = "Password required"
|
||||||
|
showFailure = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.unlockInProgress = true
|
||||||
|
errorMessage = ""
|
||||||
|
showFailure = false
|
||||||
|
|
||||||
|
Logger.log("LockContext", "Starting PAM authentication for user:", pam.user)
|
||||||
|
pam.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
PamContext {
|
||||||
|
id: pam
|
||||||
|
config: "login"
|
||||||
|
user: Quickshell.env("USER")
|
||||||
|
|
||||||
|
onPamMessage: {
|
||||||
|
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:",
|
||||||
|
responseRequired)
|
||||||
|
|
||||||
|
if (messageIsError) {
|
||||||
|
errorMessage = message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseRequired) {
|
||||||
|
Logger.log("LockContext", "Responding to PAM with password")
|
||||||
|
respond(root.currentText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onResponseRequiredChanged: {
|
||||||
|
Logger.log("LockContext", "Response required changed:", responseRequired)
|
||||||
|
if (responseRequired && root.unlockInProgress) {
|
||||||
|
Logger.log("LockContext", "Automatically responding to PAM")
|
||||||
|
respond(root.currentText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleted: result => {
|
||||||
|
Logger.log("LockContext", "PAM completed with result:", result)
|
||||||
|
if (result === PamResult.Success) {
|
||||||
|
Logger.log("LockContext", "Authentication successful")
|
||||||
|
root.unlocked()
|
||||||
|
} else {
|
||||||
|
Logger.log("LockContext", "Authentication failed")
|
||||||
|
errorMessage = "Authentication failed"
|
||||||
|
showFailure = true
|
||||||
|
root.failed()
|
||||||
|
}
|
||||||
|
root.unlockInProgress = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onError: {
|
||||||
|
Logger.log("LockContext", "PAM error:", error, "message:", message)
|
||||||
|
errorMessage = message || "Authentication error"
|
||||||
|
showFailure = true
|
||||||
|
root.unlockInProgress = false
|
||||||
|
root.failed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -308,6 +308,45 @@ ColumnLayout {
|
||||||
Settings.data.audio.visualizerType = key
|
Settings.data.audio.visualizerType = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NComboBox {
|
||||||
|
label: "Frame Rate"
|
||||||
|
description: "Target frame rate for audio visualizer. (default: 60)"
|
||||||
|
model: ListModel {
|
||||||
|
ListElement {
|
||||||
|
key: "30"
|
||||||
|
name: "30 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "60"
|
||||||
|
name: "60 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "100"
|
||||||
|
name: "100 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "120"
|
||||||
|
name: "120 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "144"
|
||||||
|
name: "144 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "165"
|
||||||
|
name: "165 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "240"
|
||||||
|
name: "240 FPS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentKey: Settings.data.audio.cavaFrameRate
|
||||||
|
onSelected: key => {
|
||||||
|
Settings.data.audio.cavaFrameRate = key
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -72,7 +71,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
|
@ -111,7 +110,6 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Show Active Window's Icon"
|
label: "Show Active Window's Icon"
|
||||||
description: "Display the app icon next to the title of the currently focused window."
|
description: "Display the app icon next to the title of the currently focused window."
|
||||||
|
|
@ -130,7 +128,6 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginL * scaling
|
Layout.topMargin: Style.marginL * scaling
|
||||||
|
|
@ -144,13 +141,14 @@ ColumnLayout {
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "Widgets Positioning"
|
text: "Widgets Positioning"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
|
Layout.bottomMargin: Style.marginS * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "Add, remove, or reorder widgets in each section of the bar using the control buttons."
|
text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
@ -165,7 +163,7 @@ ColumnLayout {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
// Left Section
|
// Left Section
|
||||||
NWidgetCard {
|
NSectionEditor {
|
||||||
sectionName: "Left"
|
sectionName: "Left"
|
||||||
widgetModel: Settings.data.bar.widgets.left
|
widgetModel: Settings.data.bar.widgets.left
|
||||||
availableWidgets: availableWidgets
|
availableWidgets: availableWidgets
|
||||||
|
|
@ -176,7 +174,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center Section
|
// Center Section
|
||||||
NWidgetCard {
|
NSectionEditor {
|
||||||
sectionName: "Center"
|
sectionName: "Center"
|
||||||
widgetModel: Settings.data.bar.widgets.center
|
widgetModel: Settings.data.bar.widgets.center
|
||||||
availableWidgets: availableWidgets
|
availableWidgets: availableWidgets
|
||||||
|
|
@ -187,7 +185,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right Section
|
// Right Section
|
||||||
NWidgetCard {
|
NSectionEditor {
|
||||||
sectionName: "Right"
|
sectionName: "Right"
|
||||||
widgetModel: Settings.data.bar.widgets.right
|
widgetModel: Settings.data.bar.widgets.right
|
||||||
availableWidgets: availableWidgets
|
availableWidgets: availableWidgets
|
||||||
|
|
@ -204,13 +202,13 @@ ColumnLayout {
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function addWidgetToSection(widgetName, section) {
|
function addWidgetToSection(widgetName, section) {
|
||||||
console.log("Adding widget", widgetName, "to section", section)
|
//Logger.log("BarTab", "Adding widget", widgetName, "to section", section)
|
||||||
var sectionArray = Settings.data.bar.widgets[section]
|
var sectionArray = Settings.data.bar.widgets[section]
|
||||||
if (sectionArray) {
|
if (sectionArray) {
|
||||||
// Create a new array to avoid modifying the original
|
// Create a new array to avoid modifying the original
|
||||||
var newArray = sectionArray.slice()
|
var newArray = sectionArray.slice()
|
||||||
newArray.push(widgetName)
|
newArray.push(widgetName)
|
||||||
console.log("Widget added. New array:", JSON.stringify(newArray))
|
//Logger.log("BarTab", "Widget added. New array:", JSON.stringify(newArray))
|
||||||
|
|
||||||
// Assign the new array
|
// Assign the new array
|
||||||
Settings.data.bar.widgets[section] = newArray
|
Settings.data.bar.widgets[section] = newArray
|
||||||
|
|
@ -218,21 +216,27 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeWidgetFromSection(section, index) {
|
function removeWidgetFromSection(section, index) {
|
||||||
console.log("Removing widget from section", section, "at index", index)
|
// Logger.log("BarTab", "Removing widget from section", section, "at index", index)
|
||||||
var sectionArray = Settings.data.bar.widgets[section]
|
var sectionArray = Settings.data.bar.widgets[section]
|
||||||
|
|
||||||
|
//Logger.log("BarTab", "Current section array:", JSON.stringify(sectionArray))
|
||||||
if (sectionArray && index >= 0 && index < sectionArray.length) {
|
if (sectionArray && index >= 0 && index < sectionArray.length) {
|
||||||
// Create a new array to avoid modifying the original
|
// Create a new array to avoid modifying the original
|
||||||
var newArray = sectionArray.slice()
|
var newArray = sectionArray.slice()
|
||||||
newArray.splice(index, 1)
|
newArray.splice(index, 1)
|
||||||
console.log("Widget removed. New array:", JSON.stringify(newArray))
|
//Logger.log("BarTab", "Widget removed. New array:", JSON.stringify(newArray))
|
||||||
|
|
||||||
// Assign the new array
|
// Assign the new array
|
||||||
Settings.data.bar.widgets[section] = newArray
|
Settings.data.bar.widgets[section] = newArray
|
||||||
|
} else {
|
||||||
|
|
||||||
|
//Logger.log("BarTab", "Invalid section or index:", section, index, "array length:",
|
||||||
|
// sectionArray ? sectionArray.length : "null")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorderWidgetInSection(section, fromIndex, toIndex) {
|
function reorderWidgetInSection(section, fromIndex, toIndex) {
|
||||||
console.log("Reordering widget in section", section, "from", fromIndex, "to", toIndex)
|
//Logger.log("BarTab", "Reordering widget in section", section, "from", fromIndex, "to", toIndex)
|
||||||
var sectionArray = Settings.data.bar.widgets[section]
|
var sectionArray = Settings.data.bar.widgets[section]
|
||||||
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0
|
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0
|
||||||
&& toIndex < sectionArray.length) {
|
&& toIndex < sectionArray.length) {
|
||||||
|
|
@ -242,36 +246,26 @@ ColumnLayout {
|
||||||
var item = newArray[fromIndex]
|
var item = newArray[fromIndex]
|
||||||
newArray.splice(fromIndex, 1)
|
newArray.splice(fromIndex, 1)
|
||||||
newArray.splice(toIndex, 0, item)
|
newArray.splice(toIndex, 0, item)
|
||||||
console.log("Widget reordered. New array:", JSON.stringify(newArray))
|
Logger.log("BarTab", "Widget reordered. New array:", JSON.stringify(newArray))
|
||||||
|
|
||||||
// Assign the new array
|
// Assign the new array
|
||||||
Settings.data.bar.widgets[section] = newArray
|
Settings.data.bar.widgets[section] = newArray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget loader for discovering available widgets
|
// Base list model for all combo boxes
|
||||||
WidgetLoader {
|
|
||||||
id: widgetLoader
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
id: availableWidgets
|
id: availableWidgets
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
discoverWidgets()
|
// Fill out availableWidgets ListModel
|
||||||
}
|
|
||||||
|
|
||||||
// Automatically discover available widgets using WidgetLoader
|
|
||||||
function discoverWidgets() {
|
|
||||||
availableWidgets.clear()
|
availableWidgets.clear()
|
||||||
|
BarWidgetRegistry.getAvailableWidgets().forEach(entry => {
|
||||||
// Use WidgetLoader to discover available widgets
|
availableWidgets.append({
|
||||||
const discoveredWidgets = widgetLoader.discoverAvailableWidgets()
|
"key": entry,
|
||||||
|
"name": entry
|
||||||
// Add discovered widgets to the ListModel
|
})
|
||||||
discoveredWidgets.forEach(widget => {
|
})
|
||||||
availableWidgets.append(widget)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,10 +125,22 @@ ColumnLayout {
|
||||||
key: "60"
|
key: "60"
|
||||||
name: "60 FPS"
|
name: "60 FPS"
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "100"
|
||||||
|
name: "100 FPS"
|
||||||
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
key: "120"
|
key: "120"
|
||||||
name: "120 FPS"
|
name: "120 FPS"
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "144"
|
||||||
|
name: "144 FPS"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "165"
|
||||||
|
name: "165 FPS"
|
||||||
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
key: "240"
|
key: "240"
|
||||||
name: "240 FPS"
|
name: "240 FPS"
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,6 @@ NBox {
|
||||||
height: 90 * scaling
|
height: 90 * scaling
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
color: trackArt.visible ? Color.mPrimary : Color.transparent
|
color: trackArt.visible ? Color.mPrimary : Color.transparent
|
||||||
border.color: trackArt.visible ? Color.mOutline : Color.transparent
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
NImageCircled {
|
NImageCircled {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ NBox {
|
||||||
icon: "image"
|
icon: "image"
|
||||||
tooltipText: "Open Wallpaper Selector"
|
tooltipText: "Open Wallpaper Selector"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
var settingsPanel = PanelService.getPanel("settingsPanel")
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
|
||||||
settingsPanel.open(screen)
|
settingsPanel.open(screen)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,10 @@ NBox {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
NText {
|
NText {
|
||||||
text: Qt.formatDateTime(new Date(LocationService.data.weather.daily.time[index]), "ddd")
|
text: {
|
||||||
|
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
|
||||||
|
return Qt.formatDateTime(weatherDate, "ddd")
|
||||||
|
}
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
}
|
}
|
||||||
NIcon {
|
NIcon {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ Variants {
|
||||||
readonly property real scaling: ScalingService.scale(screen)
|
readonly property real scaling: ScalingService.scale(screen)
|
||||||
screen: modelData
|
screen: modelData
|
||||||
|
|
||||||
|
// Only show on screens that have notifications enabled
|
||||||
|
visible: modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|
||||||
|
|| (Settings.data.notifications.monitors.length === 0)) : false
|
||||||
|
|
||||||
// Position based on bar location, like Notification popup does
|
// Position based on bar location, like Notification popup does
|
||||||
anchors {
|
anchors {
|
||||||
top: Settings.data.bar.position === "top"
|
top: Settings.data.bar.position === "top"
|
||||||
|
|
@ -51,11 +55,15 @@ Variants {
|
||||||
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
|
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Register this toast with the service
|
// Only register toasts for screens that have notifications enabled
|
||||||
ToastService.currentToast = toast
|
if (modelData ? (Settings.data.notifications.monitors.includes(modelData.name)
|
||||||
|
|| (Settings.data.notifications.monitors.length === 0)) : false) {
|
||||||
|
// Register this toast with the service
|
||||||
|
ToastService.allToasts.push(toast)
|
||||||
|
|
||||||
// Connect dismissal signal
|
// Connect dismissal signal
|
||||||
toast.dismissed.connect(ToastService.onToastDismissed)
|
toast.dismissed.connect(ToastService.onToastDismissed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
README.md
28
README.md
|
|
@ -79,6 +79,7 @@ Features a modern modular architecture with a status bar, notification system, c
|
||||||
- `gpu-screen-recorder` - Screen recording functionality
|
- `gpu-screen-recorder` - Screen recording functionality
|
||||||
- `brightnessctl` - For internal/laptop monitor brightness
|
- `brightnessctl` - For internal/laptop monitor brightness
|
||||||
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
|
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
|
||||||
|
If you want to use the ArchUpdater Widget, make sure you have any polkit agent installed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -100,8 +101,30 @@ mkdir -p ~/.config/quickshell && curl -sL https://github.com/noctalia-dev/noctal
|
||||||
# Start the shell
|
# Start the shell
|
||||||
qs
|
qs
|
||||||
|
|
||||||
# Toggle launcher
|
# Launcher
|
||||||
qs ipc call appLauncher toggle
|
qs ipc call launcher toggle
|
||||||
|
|
||||||
|
# SidePanel
|
||||||
|
qs ipc call sidePanel toggle
|
||||||
|
|
||||||
|
# Clipboard History
|
||||||
|
qs ipc call launcher clipboard
|
||||||
|
|
||||||
|
# Calculator
|
||||||
|
qs ipc call launcher calculator
|
||||||
|
|
||||||
|
# Brightness
|
||||||
|
qs ipc call brightness increase
|
||||||
|
qs ipc call brightness decrease
|
||||||
|
|
||||||
|
# Power Panel
|
||||||
|
qs ipc call powerPanel toggle
|
||||||
|
|
||||||
|
# Idle Inhibitor
|
||||||
|
qs ipc call idleInhibitor toggle
|
||||||
|
|
||||||
|
# Settings Window
|
||||||
|
qs ipc call settings toggle
|
||||||
|
|
||||||
# Toggle lock screen
|
# Toggle lock screen
|
||||||
qs ipc call lockScreen toggle
|
qs ipc call lockScreen toggle
|
||||||
|
|
@ -249,6 +272,7 @@ While I actually didn't want to accept donations, more and more people are askin
|
||||||
|
|
||||||
Thank you to everyone who supports me and this project 💜!
|
Thank you to everyone who supports me and this project 💜!
|
||||||
* Gohma
|
* Gohma
|
||||||
|
* <a href="https://pika-os.com/" target="_blank">PikaOS</a>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
156
Services/ArchUpdaterService.qml
Normal file
156
Services/ArchUpdaterService.qml
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: updateService
|
||||||
|
|
||||||
|
// Core properties
|
||||||
|
readonly property bool busy: checkupdatesProcess.running
|
||||||
|
readonly property int updates: updatePackages.length
|
||||||
|
property var updatePackages: []
|
||||||
|
property var selectedPackages: []
|
||||||
|
property int selectedPackagesCount: 0
|
||||||
|
property bool updateInProgress: false
|
||||||
|
|
||||||
|
// Process for checking updates
|
||||||
|
Process {
|
||||||
|
id: checkupdatesProcess
|
||||||
|
command: ["checkupdates"]
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (exitCode !== 0 && exitCode !== 2) {
|
||||||
|
console.warn("[UpdateService] checkupdates failed (code:", exitCode, ")")
|
||||||
|
updatePackages = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
parseCheckupdatesOutput(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse checkupdates output
|
||||||
|
function parseCheckupdatesOutput(output) {
|
||||||
|
const lines = output.trim().split('\n').filter(line => line.trim())
|
||||||
|
const packages = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const m = line.match(/^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/)
|
||||||
|
if (m) {
|
||||||
|
packages.push({
|
||||||
|
"name": m[1],
|
||||||
|
"oldVersion": m[2],
|
||||||
|
"newVersion": m[3],
|
||||||
|
"description": `${m[1]} ${m[2]} -> ${m[3]}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePackages = packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
function doPoll() {
|
||||||
|
if (busy)
|
||||||
|
return
|
||||||
|
checkupdatesProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all packages
|
||||||
|
function runUpdate() {
|
||||||
|
if (updates === 0) {
|
||||||
|
doPoll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInProgress = true
|
||||||
|
Quickshell.execDetached(["pkexec", "pacman", "-Syu", "--noconfirm"])
|
||||||
|
|
||||||
|
// Refresh after updates with multiple attempts
|
||||||
|
refreshAfterUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selected packages
|
||||||
|
function runSelectiveUpdate() {
|
||||||
|
if (selectedPackages.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
updateInProgress = true
|
||||||
|
const command = ["pkexec", "pacman", "-S", "--noconfirm"].concat(selectedPackages)
|
||||||
|
Quickshell.execDetached(command)
|
||||||
|
|
||||||
|
// Clear selection and refresh
|
||||||
|
selectedPackages = []
|
||||||
|
selectedPackagesCount = 0
|
||||||
|
refreshAfterUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package selection functions
|
||||||
|
function togglePackageSelection(packageName) {
|
||||||
|
const index = selectedPackages.indexOf(packageName)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedPackages.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
selectedPackages.push(packageName)
|
||||||
|
}
|
||||||
|
selectedPackagesCount = selectedPackages.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllPackages() {
|
||||||
|
selectedPackages = updatePackages.map(pkg => pkg.name)
|
||||||
|
selectedPackagesCount = selectedPackages.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAllPackages() {
|
||||||
|
selectedPackages = []
|
||||||
|
selectedPackagesCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPackageSelected(packageName) {
|
||||||
|
return selectedPackages.indexOf(packageName) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robust refresh after updates
|
||||||
|
function refreshAfterUpdate() {
|
||||||
|
// First refresh attempt after 3 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
// Second refresh attempt after 8 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
}, 8000)
|
||||||
|
|
||||||
|
// Third refresh attempt after 15 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
updateInProgress = false
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
// Final refresh attempt after 30 seconds
|
||||||
|
Qt.callLater(() => {
|
||||||
|
doPoll()
|
||||||
|
}, 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification helper
|
||||||
|
function notify(title, body) {
|
||||||
|
Quickshell.execDetached(["notify-send", "-a", "UpdateService", "-i", "system-software-update", title, body])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-poll every 15 minutes
|
||||||
|
Timer {
|
||||||
|
interval: 15 * 60 * 1000 // 15 minutes
|
||||||
|
repeat: true
|
||||||
|
running: true
|
||||||
|
onTriggered: doPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
Component.onCompleted: doPoll()
|
||||||
|
}
|
||||||
99
Services/BarWidgetRegistry.qml
Normal file
99
Services/BarWidgetRegistry.qml
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Modules.Bar.Widgets
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Widget registry object mapping widget names to components
|
||||||
|
property var widgets: ({
|
||||||
|
"ActiveWindow": activeWindowComponent,
|
||||||
|
// "ArchUpdater": archUpdaterComponent,
|
||||||
|
"Battery": batteryComponent,
|
||||||
|
"Bluetooth": bluetoothComponent,
|
||||||
|
"Brightness": brightnessComponent,
|
||||||
|
"Clock": clockComponent,
|
||||||
|
"KeyboardLayout": keyboardLayoutComponent,
|
||||||
|
"MediaMini": mediaMiniComponent,
|
||||||
|
"NotificationHistory": notificationHistoryComponent,
|
||||||
|
"PowerProfile": powerProfileComponent,
|
||||||
|
"ScreenRecorderIndicator": screenRecorderIndicatorComponent,
|
||||||
|
"SidePanelToggle": sidePanelToggleComponent,
|
||||||
|
"SystemMonitor": systemMonitorComponent,
|
||||||
|
"Tray": trayComponent,
|
||||||
|
"Volume": volumeComponent,
|
||||||
|
"WiFi": wiFiComponent,
|
||||||
|
"Workspace": workspaceComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
// Component definitions - these are loaded once at startup
|
||||||
|
property Component activeWindowComponent: Component {
|
||||||
|
ActiveWindow {}
|
||||||
|
}
|
||||||
|
// property Component archUpdaterComponent: Component {
|
||||||
|
// ArchUpdater {}
|
||||||
|
// }
|
||||||
|
property Component batteryComponent: Component {
|
||||||
|
Battery {}
|
||||||
|
}
|
||||||
|
property Component bluetoothComponent: Component {
|
||||||
|
Bluetooth {}
|
||||||
|
}
|
||||||
|
property Component brightnessComponent: Component {
|
||||||
|
Brightness {}
|
||||||
|
}
|
||||||
|
property Component clockComponent: Component {
|
||||||
|
Clock {}
|
||||||
|
}
|
||||||
|
property Component keyboardLayoutComponent: Component {
|
||||||
|
KeyboardLayout {}
|
||||||
|
}
|
||||||
|
property Component mediaMiniComponent: Component {
|
||||||
|
MediaMini {}
|
||||||
|
}
|
||||||
|
property Component notificationHistoryComponent: Component {
|
||||||
|
NotificationHistory {}
|
||||||
|
}
|
||||||
|
property Component powerProfileComponent: Component {
|
||||||
|
PowerProfile {}
|
||||||
|
}
|
||||||
|
property Component screenRecorderIndicatorComponent: Component {
|
||||||
|
ScreenRecorderIndicator {}
|
||||||
|
}
|
||||||
|
property Component sidePanelToggleComponent: Component {
|
||||||
|
SidePanelToggle {}
|
||||||
|
}
|
||||||
|
property Component systemMonitorComponent: Component {
|
||||||
|
SystemMonitor {}
|
||||||
|
}
|
||||||
|
property Component trayComponent: Component {
|
||||||
|
Tray {}
|
||||||
|
}
|
||||||
|
property Component volumeComponent: Component {
|
||||||
|
Volume {}
|
||||||
|
}
|
||||||
|
property Component wiFiComponent: Component {
|
||||||
|
WiFi {}
|
||||||
|
}
|
||||||
|
property Component workspaceComponent: Component {
|
||||||
|
Workspace {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Helper function to get widget component by name
|
||||||
|
function getWidget(name) {
|
||||||
|
return widgets[name] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if widget exists
|
||||||
|
function hasWidget(name) {
|
||||||
|
return name in widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of available widget names
|
||||||
|
function getAvailableWidgets() {
|
||||||
|
return Object.keys(widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ Singleton {
|
||||||
property var config: ({
|
property var config: ({
|
||||||
"general": {
|
"general": {
|
||||||
"bars": barsCount,
|
"bars": barsCount,
|
||||||
"framerate": 60,
|
"framerate": Settings.data.audio.cavaFrameRate,
|
||||||
"autosens": 1,
|
"autosens": 1,
|
||||||
"sensitivity": 100,
|
"sensitivity": 100,
|
||||||
"lower_cutoff_freq": 50,
|
"lower_cutoff_freq": 50,
|
||||||
|
|
@ -38,7 +38,7 @@ Singleton {
|
||||||
id: process
|
id: process
|
||||||
stdinEnabled: true
|
stdinEnabled: true
|
||||||
running: (Settings.data.audio.visualizerType !== "none")
|
running: (Settings.data.audio.visualizerType !== "none")
|
||||||
&& (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava
|
&& (PanelService.getPanel("sidePanel").active || Settings.data.audio.showMiniplayerCava
|
||||||
|| (PanelService.lockScreen && PanelService.lockScreen.active))
|
|| (PanelService.lockScreen && PanelService.lockScreen.active))
|
||||||
command: ["cava", "-p", "/dev/stdin"]
|
command: ["cava", "-p", "/dev/stdin"]
|
||||||
onExited: {
|
onExited: {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Singleton {
|
||||||
property ListModel workspaces: ListModel {}
|
property ListModel workspaces: ListModel {}
|
||||||
property var windows: []
|
property var windows: []
|
||||||
property int focusedWindowIndex: -1
|
property int focusedWindowIndex: -1
|
||||||
property string focusedWindowTitle: "(No active window)"
|
property string focusedWindowTitle: "n/a"
|
||||||
property bool inOverview: false
|
property bool inOverview: false
|
||||||
|
|
||||||
// Generic events
|
// Generic events
|
||||||
|
|
@ -27,6 +27,7 @@ Singleton {
|
||||||
signal activeWindowChanged
|
signal activeWindowChanged
|
||||||
signal overviewStateChanged
|
signal overviewStateChanged
|
||||||
signal windowListChanged
|
signal windowListChanged
|
||||||
|
signal windowTitleChanged
|
||||||
|
|
||||||
// Compositor detection
|
// Compositor detection
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|
@ -308,9 +309,18 @@ Singleton {
|
||||||
|
|
||||||
// Update focused window index if this window is focused
|
// Update focused window index if this window is focused
|
||||||
if (newWindow.isFocused) {
|
if (newWindow.isFocused) {
|
||||||
|
const oldFocusedIndex = focusedWindowIndex
|
||||||
focusedWindowIndex = windows.findIndex(w => w.id === windowData.id)
|
focusedWindowIndex = windows.findIndex(w => w.id === windowData.id)
|
||||||
updateFocusedWindowTitle()
|
updateFocusedWindowTitle()
|
||||||
activeWindowChanged()
|
|
||||||
|
// Only emit activeWindowChanged if the focused window actually changed
|
||||||
|
if (oldFocusedIndex !== focusedWindowIndex) {
|
||||||
|
activeWindowChanged()
|
||||||
|
}
|
||||||
|
} else if (existingIndex >= 0 && existingIndex === focusedWindowIndex) {
|
||||||
|
// If this is the currently focused window (but not newly focused),
|
||||||
|
// still update the title in case it changed, but don't emit activeWindowChanged
|
||||||
|
updateFocusedWindowTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
windowListChanged()
|
windowListChanged()
|
||||||
|
|
@ -449,11 +459,17 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFocusedWindowTitle() {
|
function updateFocusedWindowTitle() {
|
||||||
|
const oldTitle = focusedWindowTitle
|
||||||
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
||||||
focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)"
|
focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)"
|
||||||
} else {
|
} else {
|
||||||
focusedWindowTitle = "(No active window)"
|
focusedWindowTitle = "(No active window)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit signal if title actually changed
|
||||||
|
if (oldTitle !== focusedWindowTitle) {
|
||||||
|
windowTitleChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic workspace switching
|
// Generic workspace switching
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
// GitHub API logic and caching
|
// GitHub API logic and caching
|
||||||
Singleton {
|
Singleton {
|
||||||
|
|
|
||||||
112
Services/KeyboardLayoutService.qml
Normal file
112
Services/KeyboardLayoutService.qml
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string currentLayout: "Unknown"
|
||||||
|
property int updateInterval: 1000 // Update every second
|
||||||
|
|
||||||
|
// Timer to periodically update the layout
|
||||||
|
Timer {
|
||||||
|
id: updateTimer
|
||||||
|
interval: updateInterval
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process to get current keyboard layout using niri msg (Wayland native)
|
||||||
|
Process {
|
||||||
|
id: niriLayoutProcess
|
||||||
|
running: false
|
||||||
|
command: ["niri", "msg", "-j", "keyboard-layouts"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text)
|
||||||
|
const layoutName = data.names[data.current_idx]
|
||||||
|
root.currentLayout = mapLayoutNameToCode(layoutName)
|
||||||
|
} catch (e) {
|
||||||
|
root.currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process to get current keyboard layout using hyprctl (Hyprland)
|
||||||
|
Process {
|
||||||
|
id: hyprlandLayoutProcess
|
||||||
|
running: false
|
||||||
|
command: ["hyprctl", "-j", "devices"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text)
|
||||||
|
// Find the main keyboard and get its active keymap
|
||||||
|
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
|
||||||
|
if (mainKeyboard && mainKeyboard.active_keymap) {
|
||||||
|
root.currentLayout = mapLayoutNameToCode(mainKeyboard.active_keymap)
|
||||||
|
} else {
|
||||||
|
root.currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
root.currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout name to ISO code mapping
|
||||||
|
property var layoutMap: {
|
||||||
|
"German": "de",
|
||||||
|
"English (US)": "us",
|
||||||
|
"English (UK)": "gb",
|
||||||
|
"French": "fr",
|
||||||
|
"Spanish": "es",
|
||||||
|
"Italian": "it",
|
||||||
|
"Portuguese (Brazil)": "br",
|
||||||
|
"Portuguese": "pt",
|
||||||
|
"Russian": "ru",
|
||||||
|
"Polish": "pl",
|
||||||
|
"Swedish": "se",
|
||||||
|
"Norwegian": "no",
|
||||||
|
"Danish": "dk",
|
||||||
|
"Finnish": "fi",
|
||||||
|
"Hungarian": "hu",
|
||||||
|
"Turkish": "tr",
|
||||||
|
"Czech": "cz",
|
||||||
|
"Slovak": "sk",
|
||||||
|
"Japanese": "jp",
|
||||||
|
"Korean": "kr",
|
||||||
|
"Chinese": "cn"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map layout names to ISO codes
|
||||||
|
function mapLayoutNameToCode(layoutName) {
|
||||||
|
return layoutMap[layoutName] || layoutName // fallback to raw name if not found
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Logger.log("KeyboardLayout", "Service started")
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLayout() {
|
||||||
|
if (CompositorService.isHyprland) {
|
||||||
|
hyprlandLayoutProcess.running = true
|
||||||
|
} else if (CompositorService.isNiri) {
|
||||||
|
niriLayoutProcess.running = true
|
||||||
|
} else {
|
||||||
|
currentLayout = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
// Weather logic and caching
|
// Weather logic and caching
|
||||||
Singleton {
|
Singleton {
|
||||||
|
|
@ -109,8 +110,8 @@ Singleton {
|
||||||
|
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
function _geocodeLocation(locationName, callback, errorCallback) {
|
function _geocodeLocation(locationName, callback, errorCallback) {
|
||||||
Logger.log("Location", "Geocoding from api.open-meteo.com")
|
Logger.log("Location", "Geocoding location name")
|
||||||
var geoUrl = "https://geocoding-api.open-meteo.com/v1/search?name=" + encodeURIComponent(
|
var geoUrl = "https://assets.noctalia.dev/geocode.php?city=" + encodeURIComponent(
|
||||||
locationName) + "&language=en&format=json"
|
locationName) + "&language=en&format=json"
|
||||||
var xhr = new XMLHttpRequest()
|
var xhr = new XMLHttpRequest()
|
||||||
xhr.onreadystatechange = function () {
|
xhr.onreadystatechange = function () {
|
||||||
|
|
@ -119,8 +120,8 @@ Singleton {
|
||||||
try {
|
try {
|
||||||
var geoData = JSON.parse(xhr.responseText)
|
var geoData = JSON.parse(xhr.responseText)
|
||||||
// Logger.logJSON.stringify(geoData))
|
// Logger.logJSON.stringify(geoData))
|
||||||
if (geoData.results && geoData.results.length > 0) {
|
if (geoData.lat != null) {
|
||||||
callback(geoData.results[0].latitude, geoData.results[0].longitude)
|
callback(geoData.lat, geoData.lng)
|
||||||
} else {
|
} else {
|
||||||
errorCallback("Location", "could not resolve location name")
|
errorCallback("Location", "could not resolve location name")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ Singleton {
|
||||||
property string detectedInterface: ""
|
property string detectedInterface: ""
|
||||||
property string lastConnectedNetwork: ""
|
property string lastConnectedNetwork: ""
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
|
property bool ethernet: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Logger.log("Network", "Service started")
|
Logger.log("Network", "Service started")
|
||||||
|
|
@ -43,6 +44,7 @@ Singleton {
|
||||||
|
|
||||||
function refreshNetworks() {
|
function refreshNetworks() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
checkEthernet.running = true
|
||||||
existingNetwork.running = true
|
existingNetwork.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,6 +418,24 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property Process checkEthernet: Process {
|
||||||
|
id: checkEthernet
|
||||||
|
running: false
|
||||||
|
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
var lines = text.split("\n")
|
||||||
|
for (var i = 0; i < lines.length; ++i) {
|
||||||
|
var parts = lines[i].split(":")
|
||||||
|
if (parts[1] === "ethernet" && parts[2] === "connected") {
|
||||||
|
root.ethernet = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property Process addConnectionProcess: Process {
|
property Process addConnectionProcess: Process {
|
||||||
id: addConnectionProcess
|
id: addConnectionProcess
|
||||||
property string ifname: ""
|
property string ifname: ""
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
pragma Singleton
|
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,38 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// A ref. to the sidePanel, so it's accessible from other services
|
// A ref. to the lockScreen, so it's accessible from anywhere
|
||||||
property var sidePanel: null
|
// This is not a panel...
|
||||||
|
|
||||||
// A ref. to the lockScreen, so it's accessible from other services
|
|
||||||
property var lockScreen: null
|
property var lockScreen: null
|
||||||
|
|
||||||
// Currently opened panel
|
// Currently opened panel
|
||||||
property var openedPanel: null
|
property var openedPanel: null
|
||||||
|
|
||||||
function registerOpen(panel) {
|
property var registeredPanels: ({})
|
||||||
|
|
||||||
|
// Register this panel
|
||||||
|
function registerPanel(panel) {
|
||||||
|
registeredPanels[panel.objectName] = panel
|
||||||
|
Logger.log("PanelService", "Registered:", panel.objectName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a panel
|
||||||
|
function getPanel(name) {
|
||||||
|
return registeredPanels[name] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a panel exists
|
||||||
|
function hasPanel(name) {
|
||||||
|
return name in registeredPanels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to keep only one panel open at any time
|
||||||
|
function willOpenPanel(panel) {
|
||||||
if (openedPanel && openedPanel != panel) {
|
if (openedPanel && openedPanel != panel) {
|
||||||
openedPanel.close()
|
openedPanel.close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,22 @@ Singleton {
|
||||||
videoDir += "/"
|
videoDir += "/"
|
||||||
}
|
}
|
||||||
outputPath = videoDir + filename
|
outputPath = videoDir + filename
|
||||||
var command = `gpu-screen-recorder -w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath}`
|
var flags = `-w ${settings.videoSource} -f ${settings.frameRate} -ac ${settings.audioCodec} -k ${settings.videoCodec} -a ${settings.audioSource} -q ${settings.quality} -cursor ${settings.showCursor ? "yes" : "no"} -cr ${settings.colorRange} -o ${outputPath}`
|
||||||
|
var command = `
|
||||||
|
_gpuscreenrecorder_flatpak_installed() {
|
||||||
|
flatpak list --app | grep -q "com.dec05eba.gpu_screen_recorder"
|
||||||
|
}
|
||||||
|
if command -v gpu-screen-recorder >/dev/null 2>&1; then
|
||||||
|
gpu-screen-recorder ${flags}
|
||||||
|
elif command -v flatpak >/dev/null 2>&1 && _gpuscreenrecorder_flatpak_installed; then
|
||||||
|
flatpak run --command=gpu-screen-recorder --file-forwarding com.dec05eba.gpu_screen_recorder ${flags}
|
||||||
|
else
|
||||||
|
notify-send "gpu-screen-recorder not installed!" -u critical
|
||||||
|
fi`
|
||||||
|
|
||||||
//Logger.log("ScreenRecorder", command)
|
//Logger.log("ScreenRecorder", command)
|
||||||
Quickshell.execDetached(["sh", "-c", command])
|
Quickshell.execDetached(["sh", "-c", command])
|
||||||
Logger.log("ScreenRecorder", "Started recording")
|
Logger.log("ScreenRecorder", "Started recording")
|
||||||
//Logger.log("ScreenRecorder", command)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop recording using Quickshell.execDetached
|
// Stop recording using Quickshell.execDetached
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,77 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
|
|
||||||
QtObject {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Queue of pending toast messages
|
// Queue of pending toast messages
|
||||||
property var messageQueue: []
|
property var messageQueue: []
|
||||||
property bool isShowingToast: false
|
property bool isShowingToast: false
|
||||||
|
|
||||||
// Reference to the current toast instance (set by ToastManager)
|
// Reference to all toast instances (set by ToastManager)
|
||||||
property var currentToast: null
|
property var allToasts: []
|
||||||
|
|
||||||
|
// Properties for command checking
|
||||||
|
property var commandCheckCallback: null
|
||||||
|
property string commandCheckSuccessMessage: ""
|
||||||
|
property string commandCheckFailMessage: ""
|
||||||
|
|
||||||
|
// Properties for command running
|
||||||
|
property var commandRunCallback: null
|
||||||
|
property string commandRunSuccessMessage: ""
|
||||||
|
property string commandRunFailMessage: ""
|
||||||
|
|
||||||
|
// Properties for delayed toast
|
||||||
|
property string delayedToastMessage: ""
|
||||||
|
property string delayedToastType: "notice"
|
||||||
|
|
||||||
|
// Process for command checking
|
||||||
|
Process {
|
||||||
|
id: commandCheckProcess
|
||||||
|
command: ["which", "test"]
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
showNotice(commandCheckSuccessMessage)
|
||||||
|
if (commandCheckCallback)
|
||||||
|
commandCheckCallback()
|
||||||
|
} else {
|
||||||
|
showWarning(commandCheckFailMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process for command running
|
||||||
|
Process {
|
||||||
|
id: commandRunProcess
|
||||||
|
command: ["echo", "test"]
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
showNotice(commandRunSuccessMessage)
|
||||||
|
if (commandRunCallback)
|
||||||
|
commandRunCallback()
|
||||||
|
} else {
|
||||||
|
showWarning(commandRunFailMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer for delayed toast
|
||||||
|
Timer {
|
||||||
|
id: delayedToastTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
showToast(delayedToastMessage, delayedToastType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Methods to show different types of messages
|
// Methods to show different types of messages
|
||||||
function showNotice(label, description = "", persistent = false, duration = 3000) {
|
function showNotice(label, description = "", persistent = false, duration = 3000) {
|
||||||
|
|
@ -25,37 +84,14 @@ QtObject {
|
||||||
|
|
||||||
// Utility function to check if a command exists and show appropriate toast
|
// Utility function to check if a command exists and show appropriate toast
|
||||||
function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) {
|
function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) {
|
||||||
var checkProcess = Qt.createQmlObject(`
|
// Store callback for use in the process
|
||||||
import QtQuick
|
commandCheckCallback = onSuccess
|
||||||
import Quickshell.Io
|
commandCheckSuccessMessage = successMessage
|
||||||
Process {
|
commandCheckFailMessage = failMessage
|
||||||
id: checkProc
|
|
||||||
command: ["which", "${command}"]
|
|
||||||
running: true
|
|
||||||
|
|
||||||
property var onSuccessCallback: null
|
// Start the command check process
|
||||||
property bool hasFinished: false
|
commandCheckProcess.command = ["which", command]
|
||||||
|
commandCheckProcess.running = true
|
||||||
onExited: {
|
|
||||||
if (!hasFinished) {
|
|
||||||
hasFinished = true
|
|
||||||
if (exitCode === 0) {
|
|
||||||
ToastService.showNotice("${successMessage}")
|
|
||||||
if (onSuccessCallback) onSuccessCallback()
|
|
||||||
} else {
|
|
||||||
ToastService.showWarning("${failMessage}")
|
|
||||||
}
|
|
||||||
checkProc.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback collectors to prevent issues
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
stderr: StdioCollector {}
|
|
||||||
}
|
|
||||||
`, root)
|
|
||||||
|
|
||||||
checkProcess.onSuccessCallback = onSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple function to show a random toast (useful for testing or fun messages)
|
// Simple function to show a random toast (useful for testing or fun messages)
|
||||||
|
|
@ -95,37 +131,14 @@ QtObject {
|
||||||
|
|
||||||
// Generic command runner with toast feedback
|
// Generic command runner with toast feedback
|
||||||
function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) {
|
function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) {
|
||||||
var fullCommand = [command].concat(args || [])
|
// Store callback for use in the process
|
||||||
var runProcess = Qt.createQmlObject(`
|
commandRunCallback = onSuccess
|
||||||
import QtQuick
|
commandRunSuccessMessage = successMessage
|
||||||
import Quickshell.Io
|
commandRunFailMessage = failMessage
|
||||||
Process {
|
|
||||||
id: runProc
|
|
||||||
command: ${JSON.stringify(fullCommand)}
|
|
||||||
running: true
|
|
||||||
|
|
||||||
property var onSuccessCallback: null
|
// Start the command run process
|
||||||
property bool hasFinished: false
|
commandRunProcess.command = [command].concat(args || [])
|
||||||
|
commandRunProcess.running = true
|
||||||
onExited: {
|
|
||||||
if (!hasFinished) {
|
|
||||||
hasFinished = true
|
|
||||||
if (exitCode === 0) {
|
|
||||||
ToastService.showNotice("${successMessage}")
|
|
||||||
if (onSuccessCallback) onSuccessCallback()
|
|
||||||
} else {
|
|
||||||
ToastService.showWarning("${failMessage}")
|
|
||||||
}
|
|
||||||
runProc.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
stderr: StdioCollector {}
|
|
||||||
}
|
|
||||||
`, root)
|
|
||||||
|
|
||||||
runProcess.onSuccessCallback = onSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a file/directory exists
|
// Check if a file/directory exists
|
||||||
|
|
@ -135,18 +148,10 @@ QtObject {
|
||||||
|
|
||||||
// Show toast after a delay (useful for delayed feedback)
|
// Show toast after a delay (useful for delayed feedback)
|
||||||
function delayedToast(message, type = "notice", delayMs = 1000) {
|
function delayedToast(message, type = "notice", delayMs = 1000) {
|
||||||
var timer = Qt.createQmlObject(`
|
delayedToastMessage = message
|
||||||
import QtQuick
|
delayedToastType = type
|
||||||
Timer {
|
delayedToastTimer.interval = delayMs
|
||||||
interval: ${delayMs}
|
delayedToastTimer.restart()
|
||||||
repeat: false
|
|
||||||
running: true
|
|
||||||
onTriggered: {
|
|
||||||
ToastService.showToast("${message}", "${type}")
|
|
||||||
destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic method to show a toast
|
// Generic method to show a toast
|
||||||
|
|
@ -171,7 +176,7 @@ QtObject {
|
||||||
|
|
||||||
// Process the message queue
|
// Process the message queue
|
||||||
function processQueue() {
|
function processQueue() {
|
||||||
if (messageQueue.length === 0 || !currentToast) {
|
if (messageQueue.length === 0 || allToasts.length === 0) {
|
||||||
isShowingToast = false
|
isShowingToast = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -184,24 +189,37 @@ QtObject {
|
||||||
var toastData = messageQueue.shift()
|
var toastData = messageQueue.shift()
|
||||||
isShowingToast = true
|
isShowingToast = true
|
||||||
|
|
||||||
// Configure and show toast
|
// Configure and show toast on all screens
|
||||||
currentToast.label = toastData.label
|
for (var i = 0; i < allToasts.length; i++) {
|
||||||
currentToast.description = toastData.description
|
var toast = allToasts[i]
|
||||||
currentToast.type = toastData.type
|
toast.label = toastData.label
|
||||||
currentToast.persistent = toastData.persistent
|
toast.description = toastData.description
|
||||||
currentToast.duration = toastData.duration
|
toast.type = toastData.type
|
||||||
currentToast.show()
|
toast.persistent = toastData.persistent
|
||||||
|
toast.duration = toastData.duration
|
||||||
|
toast.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when a toast is dismissed
|
// Called when a toast is dismissed
|
||||||
function onToastDismissed() {
|
function onToastDismissed() {
|
||||||
|
// Check if all toasts are dismissed
|
||||||
|
var allDismissed = true
|
||||||
|
for (var i = 0; i < allToasts.length; i++) {
|
||||||
|
if (allToasts[i].visible) {
|
||||||
|
allDismissed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isShowingToast = false
|
if (allDismissed) {
|
||||||
|
isShowingToast = false
|
||||||
|
|
||||||
// Small delay before showing next toast
|
// Small delay before showing next toast
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
processQueue()
|
processQueue()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all pending messages
|
// Clear all pending messages
|
||||||
|
|
@ -212,8 +230,10 @@ QtObject {
|
||||||
|
|
||||||
// Hide current toast
|
// Hide current toast
|
||||||
function hideCurrentToast() {
|
function hideCurrentToast() {
|
||||||
if (currentToast && isShowingToast) {
|
if (isShowingToast) {
|
||||||
currentToast.hide()
|
for (var i = 0; i < allToasts.length; i++) {
|
||||||
|
allToasts[i].hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
// Generic card container
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
implicitWidth: childrenRect.width
|
|
||||||
implicitHeight: childrenRect.height
|
|
||||||
|
|
||||||
color: Color.mSurface
|
|
||||||
radius: Style.radiusM * scaling
|
|
||||||
border.color: Color.mOutline
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
}
|
|
||||||
|
|
@ -8,14 +8,14 @@ import qs.Widgets
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property real preferredHeight: Style.baseWidgetSize * 1.25 * scaling
|
readonly property real preferredHeight: Style.baseWidgetSize * 1.35 * scaling
|
||||||
|
|
||||||
property string label: ""
|
property string label: ""
|
||||||
property string description: ""
|
property string description: ""
|
||||||
property ListModel model: {
|
property ListModel model: {
|
||||||
|
|
||||||
}
|
}
|
||||||
property string currentKey: ''
|
property string currentKey: ""
|
||||||
property string placeholder: ""
|
property string placeholder: ""
|
||||||
|
|
||||||
signal selected(string key)
|
signal selected(string key)
|
||||||
|
|
@ -39,7 +39,8 @@ ColumnLayout {
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: combo
|
id: combo
|
||||||
Layout.fillWidth: true
|
|
||||||
|
Layout.preferredWidth: 320 * scaling
|
||||||
Layout.preferredHeight: height
|
Layout.preferredHeight: height
|
||||||
model: model
|
model: model
|
||||||
currentIndex: findIndexByKey(currentKey)
|
currentIndex: findIndexByKey(currentKey)
|
||||||
|
|
@ -128,5 +129,13 @@ ColumnLayout {
|
||||||
radius: Style.radiusM * scaling
|
radius: Style.radiusM * scaling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the currentIndex if the currentKey is changed externalyu
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onCurrentKeyChanged() {
|
||||||
|
combo.currentIndex = root.findIndexByKey(currentKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ Loader {
|
||||||
signal opened
|
signal opened
|
||||||
signal closed
|
signal closed
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// console.log("Oh Yeah")
|
||||||
|
// console.log(objectName)
|
||||||
|
PanelService.registerPanel(root)
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// -----------------------------------------
|
||||||
function toggle(aScreen) {
|
function toggle(aScreen) {
|
||||||
if (!active || isClosing) {
|
if (!active || isClosing) {
|
||||||
|
|
@ -53,7 +59,7 @@ Loader {
|
||||||
opacityValue = 1.0
|
opacityValue = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelService.registerOpen(root)
|
PanelService.willOpenPanel(root)
|
||||||
|
|
||||||
active = true
|
active = true
|
||||||
root.opened()
|
root.opened()
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@ Item {
|
||||||
property color collapsedIconColor: Color.mOnSurface
|
property color collapsedIconColor: Color.mOnSurface
|
||||||
property real sizeMultiplier: 0.8
|
property real sizeMultiplier: 0.8
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
// When true, keep the pill expanded regardless of hover state
|
property bool forceOpen: false
|
||||||
property bool forceShown: false
|
property bool disableOpen: false
|
||||||
|
|
||||||
// Effective shown state (true if hovered/animated open or forced)
|
// Effective shown state (true if hovered/animated open or forced)
|
||||||
readonly property bool effectiveShown: forceShown || showPill
|
readonly property bool effectiveShown: forceOpen || showPill
|
||||||
|
|
||||||
signal shown
|
signal shown
|
||||||
signal hidden
|
signal hidden
|
||||||
|
|
@ -85,7 +86,7 @@ Item {
|
||||||
height: iconSize
|
height: iconSize
|
||||||
radius: width * 0.5
|
radius: width * 0.5
|
||||||
// When forced shown, match pill background; otherwise use accent when hovered
|
// When forced shown, match pill background; otherwise use accent when hovered
|
||||||
color: forceShown ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
|
@ -100,7 +101,7 @@ Item {
|
||||||
text: root.icon
|
text: root.icon
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
// When forced shown, use pill text color; otherwise accent color when hovered
|
// When forced shown, use pill text color; otherwise accent color when hovered
|
||||||
color: forceShown ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -194,18 +195,21 @@ Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (!forceShown) {
|
root.entered()
|
||||||
|
tooltip.show()
|
||||||
|
if (disableOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!forceOpen) {
|
||||||
showDelayed()
|
showDelayed()
|
||||||
}
|
}
|
||||||
tooltip.show()
|
|
||||||
root.entered()
|
|
||||||
}
|
}
|
||||||
onExited: {
|
onExited: {
|
||||||
if (!forceShown) {
|
root.exited()
|
||||||
|
if (!forceOpen) {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
tooltip.hide()
|
tooltip.hide()
|
||||||
root.exited()
|
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.clicked()
|
root.clicked()
|
||||||
|
|
@ -226,7 +230,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
if (forceShown) {
|
if (forceOpen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (showPill) {
|
if (showPill) {
|
||||||
|
|
@ -245,8 +249,8 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onForceShownChanged: {
|
onForceOpenChanged: {
|
||||||
if (forceShown) {
|
if (forceOpen) {
|
||||||
// Immediately lock open without animations
|
// Immediately lock open without animations
|
||||||
showAnim.stop()
|
showAnim.stop()
|
||||||
hideAnim.stop()
|
hideAnim.stop()
|
||||||
|
|
|
||||||
319
Widgets/NSectionEditor.qml
Normal file
319
Widgets/NSectionEditor.qml
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NBox {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string sectionName: ""
|
||||||
|
property var widgetModel: []
|
||||||
|
property var availableWidgets: []
|
||||||
|
property var scrollView: null
|
||||||
|
|
||||||
|
signal addWidget(string widgetName, string section)
|
||||||
|
signal removeWidget(string section, int index)
|
||||||
|
signal reorderWidget(string section, int fromIndex, int toIndex)
|
||||||
|
|
||||||
|
color: Color.mSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumHeight: {
|
||||||
|
var widgetCount = widgetModel.length
|
||||||
|
if (widgetCount === 0)
|
||||||
|
return 140 * scaling
|
||||||
|
|
||||||
|
var availableWidth = scrollView ? scrollView.availableWidth - (Style.marginM * scaling * 2) : 400 * scaling
|
||||||
|
var avgWidgetWidth = 150 * scaling
|
||||||
|
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
|
||||||
|
var rows = Math.ceil(widgetCount / widgetsPerRow)
|
||||||
|
|
||||||
|
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate widget color from name checksum
|
||||||
|
function getWidgetColor(name) {
|
||||||
|
const totalSum = name.split('').reduce((acc, character) => {
|
||||||
|
return acc + character.charCodeAt(0)
|
||||||
|
}, 0)
|
||||||
|
switch (totalSum % 5) {
|
||||||
|
case 0:
|
||||||
|
return Color.mPrimary
|
||||||
|
case 1:
|
||||||
|
return Color.mSecondary
|
||||||
|
case 2:
|
||||||
|
return Color.mTertiary
|
||||||
|
case 3:
|
||||||
|
return Color.mError
|
||||||
|
case 4:
|
||||||
|
return Color.mOnSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginM * scaling
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: sectionName + " Section"
|
||||||
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mSecondary
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
NComboBox {
|
||||||
|
id: comboBox
|
||||||
|
model: availableWidgets
|
||||||
|
label: ""
|
||||||
|
description: ""
|
||||||
|
placeholder: "Add widget to the " + sectionName.toLowerCase() + " section..."
|
||||||
|
onSelected: key => {
|
||||||
|
comboBox.currentKey = key
|
||||||
|
}
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "add"
|
||||||
|
|
||||||
|
colorBg: Color.mPrimary
|
||||||
|
colorFg: Color.mOnPrimary
|
||||||
|
colorBgHover: Color.mSecondary
|
||||||
|
colorFgHover: Color.mOnSecondary
|
||||||
|
enabled: comboBox.selectedKey !== ""
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: Style.marginS * scaling
|
||||||
|
onClicked: {
|
||||||
|
if (comboBox.currentKey !== "") {
|
||||||
|
addWidget(comboBox.currentKey, sectionName.toLowerCase())
|
||||||
|
comboBox.currentKey = "battery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and Drop Widget Area
|
||||||
|
Flow {
|
||||||
|
id: widgetFlow
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.minimumHeight: 65 * scaling
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
flow: Flow.LeftToRight
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: widgetModel
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: widgetItem
|
||||||
|
required property int index
|
||||||
|
required property string modelData
|
||||||
|
|
||||||
|
width: widgetContent.implicitWidth + 16 * scaling
|
||||||
|
height: 40 * scaling
|
||||||
|
radius: Style.radiusL * scaling
|
||||||
|
color: root.getWidgetColor(modelData)
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
|
// Drag properties
|
||||||
|
Drag.keys: ["widget"]
|
||||||
|
Drag.active: mouseArea.drag.active
|
||||||
|
Drag.hotSpot.x: width / 2
|
||||||
|
Drag.hotSpot.y: height / 2
|
||||||
|
|
||||||
|
// Store the widget index for drag operations
|
||||||
|
property int widgetIndex: index
|
||||||
|
|
||||||
|
// Visual feedback during drag
|
||||||
|
states: State {
|
||||||
|
when: mouseArea.drag.active
|
||||||
|
PropertyChanges {
|
||||||
|
target: widgetItem
|
||||||
|
scale: 1.1
|
||||||
|
opacity: 0.9
|
||||||
|
z: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: widgetContent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: modelData
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
color: Color.mOnPrimary
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
size: 20 * scaling
|
||||||
|
colorBorder: Color.applyOpacity(Color.mOutline, "40")
|
||||||
|
colorBg: Color.mOnSurface
|
||||||
|
colorFg: Color.mOnPrimary
|
||||||
|
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
||||||
|
colorFgHover: Color.mOnPrimary
|
||||||
|
onClicked: {
|
||||||
|
removeWidget(sectionName.toLowerCase(), index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse area for drag and drop
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
drag.target: parent
|
||||||
|
|
||||||
|
onPressed: mouse => {
|
||||||
|
// Check if the click is on the close button area
|
||||||
|
const closeButtonX = widgetContent.x + widgetContent.width - 20 * scaling
|
||||||
|
const closeButtonY = widgetContent.y
|
||||||
|
const closeButtonWidth = 20 * scaling
|
||||||
|
const closeButtonHeight = 20 * scaling
|
||||||
|
|
||||||
|
if (mouseX >= closeButtonX && mouseX <= closeButtonX + closeButtonWidth
|
||||||
|
&& mouseY >= closeButtonY && mouseY <= closeButtonY + closeButtonHeight) {
|
||||||
|
// Click is on the close button, don't start drag
|
||||||
|
mouse.accepted = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.log("NSectionEditor", `Started dragging widget: ${modelData} at index ${index}`)
|
||||||
|
// Bring to front when starting drag
|
||||||
|
widgetItem.z = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: {
|
||||||
|
Logger.log("NSectionEditor", `Released widget: ${modelData} at index ${index}`)
|
||||||
|
// Reset z-index when drag ends
|
||||||
|
widgetItem.z = 0
|
||||||
|
|
||||||
|
// Get the global mouse position
|
||||||
|
const globalDropX = mouseArea.mouseX + widgetItem.x + widgetFlow.x
|
||||||
|
const globalDropY = mouseArea.mouseY + widgetItem.y + widgetFlow.y
|
||||||
|
|
||||||
|
// Find which widget the drop position is closest to
|
||||||
|
let targetIndex = -1
|
||||||
|
let minDistance = Infinity
|
||||||
|
|
||||||
|
for (var i = 0; i < widgetModel.length; i++) {
|
||||||
|
if (i !== index) {
|
||||||
|
// Get the position of other widgets
|
||||||
|
const otherWidget = widgetFlow.children[i]
|
||||||
|
if (otherWidget && otherWidget.widgetIndex !== undefined) {
|
||||||
|
// Calculate the center of the other widget
|
||||||
|
const otherCenterX = otherWidget.x + otherWidget.width / 2 + widgetFlow.x
|
||||||
|
const otherCenterY = otherWidget.y + otherWidget.height / 2 + widgetFlow.y
|
||||||
|
|
||||||
|
// Calculate distance to the center of this widget
|
||||||
|
const distance = Math.sqrt(Math.pow(globalDropX - otherCenterX,
|
||||||
|
2) + Math.pow(globalDropY - otherCenterY, 2))
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance
|
||||||
|
targetIndex = otherWidget.widgetIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only reorder if we found a valid target and it's different from current position
|
||||||
|
if (targetIndex !== -1 && targetIndex !== index) {
|
||||||
|
const fromIndex = index
|
||||||
|
const toIndex = targetIndex
|
||||||
|
Logger.log(
|
||||||
|
"NSectionEditor",
|
||||||
|
`Dropped widget from index ${fromIndex} to position ${toIndex} (distance: ${minDistance.toFixed(
|
||||||
|
2)})`)
|
||||||
|
reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex)
|
||||||
|
} else {
|
||||||
|
Logger.log("NSectionEditor", `No valid drop target found for widget at index ${index}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop zone at the beginning (positioned absolutely)
|
||||||
|
DropArea {
|
||||||
|
id: startDropZone
|
||||||
|
width: 40 * scaling
|
||||||
|
height: 40 * scaling
|
||||||
|
x: widgetFlow.x
|
||||||
|
y: widgetFlow.y + (widgetFlow.height - height) / 2
|
||||||
|
keys: ["widget"]
|
||||||
|
z: 1001 // Above the Flow
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: startDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent
|
||||||
|
border.color: startDropZone.containsDrag ? Color.mPrimary : Color.transparent
|
||||||
|
border.width: startDropZone.containsDrag ? 2 : 0
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntered: function (drag) {
|
||||||
|
Logger.log("NSectionEditor", "Entered start drop zone")
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropped: function (drop) {
|
||||||
|
Logger.log("NSectionEditor", "Dropped on start zone")
|
||||||
|
if (drop.source && drop.source.widgetIndex !== undefined) {
|
||||||
|
const fromIndex = drop.source.widgetIndex
|
||||||
|
const toIndex = 0 // Insert at the beginning
|
||||||
|
if (fromIndex !== toIndex) {
|
||||||
|
Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to beginning`)
|
||||||
|
reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop zone at the end (positioned absolutely)
|
||||||
|
DropArea {
|
||||||
|
id: endDropZone
|
||||||
|
width: 40 * scaling
|
||||||
|
height: 40 * scaling
|
||||||
|
x: widgetFlow.x + widgetFlow.width - width
|
||||||
|
y: widgetFlow.y + (widgetFlow.height - height) / 2
|
||||||
|
keys: ["widget"]
|
||||||
|
z: 1001 // Above the Flow
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: endDropZone.containsDrag ? Color.applyOpacity(Color.mPrimary, "20") : Color.transparent
|
||||||
|
border.color: endDropZone.containsDrag ? Color.mPrimary : Color.transparent
|
||||||
|
border.width: endDropZone.containsDrag ? 2 : 0
|
||||||
|
radius: Style.radiusS * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntered: function (drag) {
|
||||||
|
Logger.log("NSectionEditor", "Entered end drop zone")
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropped: function (drop) {
|
||||||
|
Logger.log("NSectionEditor", "Dropped on end zone")
|
||||||
|
if (drop.source && drop.source.widgetIndex !== undefined) {
|
||||||
|
const fromIndex = drop.source.widgetIndex
|
||||||
|
const toIndex = widgetModel.length // Insert at the end
|
||||||
|
if (fromIndex !== toIndex) {
|
||||||
|
Logger.log("NSectionEditor", `Dropped widget from index ${fromIndex} to end`)
|
||||||
|
reorderWidget(sectionName.toLowerCase(), fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
NCard {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string sectionName: ""
|
|
||||||
property var widgetModel: []
|
|
||||||
property var availableWidgets: []
|
|
||||||
property var scrollView: null
|
|
||||||
|
|
||||||
signal addWidget(string widgetName, string section)
|
|
||||||
signal removeWidget(string section, int index)
|
|
||||||
signal reorderWidget(string section, int fromIndex, int toIndex)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: {
|
|
||||||
var widgetCount = widgetModel.length
|
|
||||||
if (widgetCount === 0)
|
|
||||||
return 140 * scaling
|
|
||||||
|
|
||||||
var availableWidth = scrollView ? scrollView.availableWidth - (Style.marginM * scaling * 2) : 400 * scaling
|
|
||||||
var avgWidgetWidth = 150 * scaling
|
|
||||||
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
|
|
||||||
var rows = Math.ceil(widgetCount / widgetsPerRow)
|
|
||||||
|
|
||||||
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Style.marginM * scaling
|
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: sectionName + " Section"
|
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mOnSurface
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
NComboBox {
|
|
||||||
id: comboBox
|
|
||||||
width: 120 * scaling
|
|
||||||
model: availableWidgets
|
|
||||||
label: ""
|
|
||||||
description: ""
|
|
||||||
placeholder: "Add widget to " + sectionName.toLowerCase() + " section"
|
|
||||||
onSelected: key => {
|
|
||||||
comboBox.selectedKey = key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "add"
|
|
||||||
size: 24 * scaling
|
|
||||||
colorBg: Color.mPrimary
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.mPrimaryContainer
|
|
||||||
colorFgHover: Color.mOnPrimaryContainer
|
|
||||||
enabled: comboBox.selectedKey !== ""
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
onClicked: {
|
|
||||||
if (comboBox.selectedKey !== "") {
|
|
||||||
addWidget(comboBox.selectedKey, sectionName.toLowerCase())
|
|
||||||
comboBox.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.minimumHeight: 65 * scaling
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
flow: Flow.LeftToRight
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: widgetModel
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: widgetContent.implicitWidth + 16 * scaling
|
|
||||||
height: 48 * scaling
|
|
||||||
radius: Style.radiusS * scaling
|
|
||||||
color: Color.mPrimary
|
|
||||||
border.color: Color.mOutline
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: widgetContent
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Style.marginXS * scaling
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "chevron_left"
|
|
||||||
size: 20 * scaling
|
|
||||||
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
|
||||||
colorFgHover: Color.mOnPrimary
|
|
||||||
enabled: index > 0
|
|
||||||
onClicked: {
|
|
||||||
if (index > 0) {
|
|
||||||
reorderWidget(sectionName.toLowerCase(), index, index - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: modelData
|
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
|
||||||
color: Color.mOnPrimary
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "chevron_right"
|
|
||||||
size: 20 * scaling
|
|
||||||
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
|
||||||
colorFgHover: Color.mOnPrimary
|
|
||||||
enabled: index < widgetModel.length - 1
|
|
||||||
onClicked: {
|
|
||||||
if (index < widgetModel.length - 1) {
|
|
||||||
reorderWidget(sectionName.toLowerCase(), index, index + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NIconButton {
|
|
||||||
icon: "close"
|
|
||||||
size: 20 * scaling
|
|
||||||
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
|
|
||||||
colorFg: Color.mOnPrimary
|
|
||||||
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
|
|
||||||
colorFgHover: Color.mOnPrimary
|
|
||||||
onClicked: {
|
|
||||||
removeWidget(sectionName.toLowerCase(), index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
Widgets/NWidgetLoader.qml
Normal file
46
Widgets/NWidgetLoader.qml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string widgetName: ""
|
||||||
|
property var widgetProps: ({})
|
||||||
|
property bool enabled: true
|
||||||
|
|
||||||
|
// Don't reserve space unless the loaded widget is really visible
|
||||||
|
implicitWidth: loader.item ? loader.item.visible ? loader.item.implicitWidth : 0 : 0
|
||||||
|
implicitHeight: loader.item ? loader.item.visible ? loader.item.implicitHeight : 0 : 0
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: loader
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
active: enabled && widgetName !== ""
|
||||||
|
sourceComponent: {
|
||||||
|
if (!active) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return BarWidgetRegistry.getWidget(widgetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
if (item && widgetProps) {
|
||||||
|
// Apply properties to loaded widget
|
||||||
|
for (var prop in widgetProps) {
|
||||||
|
if (item.hasOwnProperty(prop)) {
|
||||||
|
item[prop] = widgetProps[prop]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
onWidgetNameChanged: {
|
||||||
|
if (widgetName && !BarWidgetRegistry.hasWidget(widgetName)) {
|
||||||
|
Logger.warn("WidgetLoader", "Widget not found in registry:", widgetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
shell.qml
85
shell.qml
|
|
@ -27,6 +27,7 @@ import qs.Modules.PowerPanel
|
||||||
import qs.Modules.SidePanel
|
import qs.Modules.SidePanel
|
||||||
import qs.Modules.Toast
|
import qs.Modules.Toast
|
||||||
import qs.Modules.WiFiPanel
|
import qs.Modules.WiFiPanel
|
||||||
|
import qs.Modules.ArchUpdaterPanel
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
|
|
@ -39,55 +40,67 @@ ShellRoot {
|
||||||
Bar {}
|
Bar {}
|
||||||
Dock {}
|
Dock {}
|
||||||
|
|
||||||
Launcher {
|
|
||||||
id: launcherPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
SidePanel {
|
|
||||||
id: sidePanel
|
|
||||||
}
|
|
||||||
|
|
||||||
Calendar {
|
|
||||||
id: calendarPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsPanel {
|
|
||||||
id: settingsPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
Notification {
|
Notification {
|
||||||
id: notification
|
id: notification
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationHistoryPanel {
|
|
||||||
id: notificationHistoryPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
LockScreen {
|
LockScreen {
|
||||||
id: lockScreen
|
id: lockScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
PowerPanel {
|
|
||||||
id: powerPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiPanel {
|
|
||||||
id: wifiPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothPanel {
|
|
||||||
id: bluetoothPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
ToastManager {}
|
ToastManager {}
|
||||||
|
|
||||||
IPCManager {}
|
IPCManager {}
|
||||||
|
|
||||||
Component.onCompleted: {
|
// ------------------------------
|
||||||
// Save a ref. to our sidePanel so we can access it from services
|
// All the panels
|
||||||
PanelService.sidePanel = sidePanel
|
Launcher {
|
||||||
|
id: launcherPanel
|
||||||
|
objectName: "launcherPanel"
|
||||||
|
}
|
||||||
|
|
||||||
// Save a ref. to our lockScreen so we can access it from services
|
SidePanel {
|
||||||
|
id: sidePanel
|
||||||
|
objectName: "sidePanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar {
|
||||||
|
id: calendarPanel
|
||||||
|
objectName: "calendarPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsPanel {
|
||||||
|
id: settingsPanel
|
||||||
|
objectName: "settingsPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationHistoryPanel {
|
||||||
|
id: notificationHistoryPanel
|
||||||
|
objectName: "notificationHistoryPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerPanel {
|
||||||
|
id: powerPanel
|
||||||
|
objectName: "powerPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiPanel {
|
||||||
|
id: wifiPanel
|
||||||
|
objectName: "wifiPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothPanel {
|
||||||
|
id: bluetoothPanel
|
||||||
|
objectName: "bluetoothPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
ArchUpdaterPanel {
|
||||||
|
id: archUpdaterPanel
|
||||||
|
objectName: "archUpdaterPanel"
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Save a ref. to our lockScreen so we can access it easily
|
||||||
PanelService.lockScreen = lockScreen
|
PanelService.lockScreen = lockScreen
|
||||||
|
|
||||||
// Ensure our singleton is created as soon as possible so we start fetching weather asap
|
// Ensure our singleton is created as soon as possible so we start fetching weather asap
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue