1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 14:05:38 -05:00

bluetooth: switch to native quickshell bluetooth service

removes dependency on bluetoothctl commands
This commit is contained in:
bbedward
2025-07-15 12:43:22 -04:00
parent 1396b0b582
commit b7da76147f
8 changed files with 1030 additions and 761 deletions

View File

@@ -14,7 +14,6 @@ Singleton {
property var applicationsByExec: ({})
property bool ready: false
// Pre-prepared fuzzy search data
property var preppedApps: []
@@ -33,7 +32,6 @@ Singleton {
function loadApplications() {
// Trigger rescan on next frame to avoid blocking
Qt.callLater(function() {
var allApps = Array.from(DesktopEntries.applications.values)
@@ -41,7 +39,6 @@ Singleton {
.filter(app => !app.noDisplay)
.sort((a, b) => a.name.localeCompare(b.name))
// Build lookup maps
var byName = {}
var byExec = {}
@@ -49,7 +46,6 @@ Singleton {
var app = applications[i]
byName[app.name.toLowerCase()] = app
// Clean exec string for lookup
var execProp = app.execString || ""
var cleanExec = execProp ? execProp.replace(/%[fFuU]/g, "").trim() : ""
if (cleanExec) {
@@ -60,7 +56,6 @@ Singleton {
applicationsByName = byName
applicationsByExec = byExec
// Prepare fuzzy search data
preppedApps = applications.map(app => ({
name: Fuzzy.prepare(app.name || ""),
comment: Fuzzy.prepare(app.comment || ""),
@@ -84,12 +79,10 @@ Singleton {
return []
}
// Use fuzzy search with both name and comment fields
var results = Fuzzy.go(query, preppedApps, {
all: false,
keys: ["name", "comment"],
scoreFn: r => {
// Prioritize name matches over comment matches
var nameScore = r[0] ? r[0].score : 0
var commentScore = r[1] ? r[1].score : 0
return nameScore > 0 ? nameScore * 0.9 + commentScore * 0.1 : commentScore * 0.5
@@ -97,7 +90,6 @@ Singleton {
limit: 50
})
// Extract the desktop entries from results
return results.map(r => r.obj.entry)
}
@@ -180,7 +172,6 @@ Singleton {
return false
}
// DesktopEntry objects have an execute() method
if (typeof app.execute === "function") {
app.execute()
return true

View File

@@ -11,16 +11,12 @@ Singleton {
property var audioSinks: []
property string currentAudioSink: ""
// Microphone properties
property int micLevel: 50
property var audioSources: []
property string currentAudioSource: ""
// Device scanning control
property bool deviceScanningEnabled: false
property bool initialScanComplete: false
// Real Audio Control
Process {
id: volumeChecker
command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
@@ -36,7 +32,6 @@ Singleton {
}
}
// Microphone level checker
Process {
id: micLevelChecker
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
@@ -68,7 +63,6 @@ Singleton {
for (let line of lines) {
line = line.trim()
// New sink starts
if (line.startsWith('Sink #')) {
if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink)
@@ -84,39 +78,31 @@ Singleton {
active: false
}
}
// Get the Name field
else if (line.startsWith('Name: ') && currentSink) {
currentSink.name = line.replace('Name: ', '').trim()
}
// Get the Description field (main display name)
else if (line.startsWith('Description: ') && currentSink) {
currentSink.description = line.replace('Description: ', '').trim()
}
// Get device.description as fallback
else if (line.includes('device.description = ') && currentSink && !currentSink.description) {
currentSink.description = line.replace('device.description = ', '').replace(/"/g, '').trim()
}
// Get node.nick as another fallback option
else if (line.includes('node.nick = ') && currentSink && !currentSink.description) {
currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim()
}
}
// Add the last sink
if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink)
}
// Process display names
for (let sink of sinks) {
let displayName = sink.description
// If no good description, try nick
if (!displayName || displayName === sink.name) {
displayName = sink.nick
}
// Still no good name? Fall back to smart defaults
if (!displayName || displayName === sink.name) {
if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers"
else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio"
@@ -136,7 +122,6 @@ Singleton {
}
}
// Audio source (microphone) lister
Process {
id: audioSourceLister
command: ["pactl", "list", "sources"]
@@ -153,7 +138,6 @@ Singleton {
for (let line of lines) {
line = line.trim()
// New source starts
if (line.startsWith('Source #')) {
if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource)
@@ -165,23 +149,19 @@ Singleton {
active: false
}
}
// Source name
else if (line.startsWith('Name: ') && currentSource) {
currentSource.name = line.replace('Name: ', '')
}
// Description (display name)
else if (line.startsWith('Description: ') && currentSource) {
let desc = line.replace('Description: ', '')
currentSource.displayName = desc
}
}
// Add the last source
if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource)
}
// Filter out monitor sources (we want actual input devices)
sources = sources.filter(source => !source.name.includes('.monitor'))
root.audioSources = sources
@@ -202,7 +182,6 @@ Singleton {
if (data.trim()) {
root.currentAudioSink = data.trim()
// Update active status in audioSinks
let updatedSinks = []
for (let sink of root.audioSinks) {
updatedSinks.push({
@@ -218,7 +197,6 @@ Singleton {
}
}
// Default source (microphone) checker
Process {
id: defaultSourceChecker
command: ["pactl", "get-default-source"]
@@ -230,7 +208,6 @@ Singleton {
if (data.trim()) {
root.currentAudioSource = data.trim()
// Update active status in audioSources
let updatedSources = []
for (let source of root.audioSources) {
updatedSources.push({
@@ -271,12 +248,10 @@ Singleton {
function setAudioSink(sinkName) {
console.log("Setting audio sink to:", sinkName)
// Use a more reliable approach instead of Qt.createQmlObject
sinkSetProcess.command = ["pactl", "set-default-sink", sinkName]
sinkSetProcess.running = true
}
// Dedicated process for setting audio sink
Process {
id: sinkSetProcess
running: false
@@ -285,7 +260,6 @@ Singleton {
console.log("Audio sink change exit code:", exitCode)
if (exitCode === 0) {
console.log("Audio sink changed successfully")
// Refresh current sink and list
defaultSinkChecker.running = true
if (root.deviceScanningEnabled) {
audioSinkLister.running = true
@@ -303,7 +277,6 @@ Singleton {
sourceSetProcess.running = true
}
// Dedicated process for setting audio source
Process {
id: sourceSetProcess
running: false
@@ -312,7 +285,6 @@ Singleton {
console.log("Audio source change exit code:", exitCode)
if (exitCode === 0) {
console.log("Audio source changed successfully")
// Refresh current source and list
defaultSourceChecker.running = true
if (root.deviceScanningEnabled) {
audioSourceLister.running = true
@@ -337,25 +309,21 @@ Singleton {
Component.onCompleted: {
console.log("AudioService: Starting initialization...")
// Do initial device scan
audioSinkLister.running = true
audioSourceLister.running = true
initialScanComplete = true
console.log("AudioService: Initialization complete")
}
// Control functions for managing device scanning
function enableDeviceScanning(enabled) {
console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled")
root.deviceScanningEnabled = enabled
if (enabled && root.initialScanComplete) {
// Immediately scan when enabled
audioSinkLister.running = true
audioSourceLister.running = true
}
}
// Manual refresh function for when user opens audio settings
function refreshDevices() {
console.log("AudioService: Manual device refresh triggered")
audioSinkLister.running = true

View File

@@ -7,7 +7,6 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
// Battery properties
property bool batteryAvailable: false
property int batteryLevel: 0
property string batteryStatus: "Unknown"
@@ -20,8 +19,6 @@ Singleton {
property int batteryCapacity: 0
property var powerProfiles: []
property string activePowerProfile: ""
// Check if battery is available
Process {
id: batteryAvailabilityChecker
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
@@ -58,7 +55,6 @@ Singleton {
if (text.trim() && text.trim() !== "fallback") {
parseBatteryInfo(text.trim())
} else {
// Fallback to simple methods
fallbackBatteryChecker.running = true
}
}
@@ -72,7 +68,6 @@ Singleton {
}
}
// Fallback battery checker using /sys files
Process {
id: fallbackBatteryChecker
command: ["bash", "-c", "if [ -f /sys/class/power_supply/BAT0/capacity ]; then BAT=BAT0; elif [ -f /sys/class/power_supply/BAT1/capacity ]; then BAT=BAT1; else echo 'no-battery'; exit 1; fi; echo \"percentage: $(cat /sys/class/power_supply/$BAT/capacity)%\"; echo \"state: $(cat /sys/class/power_supply/$BAT/status 2>/dev/null || echo Unknown)\"; if [ -f /sys/class/power_supply/$BAT/technology ]; then echo \"technology: $(cat /sys/class/power_supply/$BAT/technology)\"; fi; if [ -f /sys/class/power_supply/$BAT/cycle_count ]; then echo \"cycle-count: $(cat /sys/class/power_supply/$BAT/cycle_count)\"; fi"]
@@ -87,7 +82,6 @@ Singleton {
}
}
// Power profiles checker (for systems with power-profiles-daemon)
Process {
id: powerProfilesChecker
command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"]
@@ -170,7 +164,6 @@ Singleton {
for (let line of lines) {
line = line.trim()
if (line.includes('*')) {
// Active profile
let profileName = line.replace('*', '').trim()
if (profileName.includes(':')) {
profileName = profileName.split(':')[0].trim()
@@ -248,7 +241,6 @@ Singleton {
}
// Update timer
Timer {
interval: 30000
running: root.batteryAvailable

View File

@@ -1,6 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Bluetooth
pragma Singleton
pragma ComponentBehavior: Bound
@@ -9,410 +9,466 @@ Singleton {
property bool bluetoothEnabled: false
property bool bluetoothAvailable: false
property var bluetoothDevices: []
property var availableDevices: []
readonly property list<BluetoothDevice> bluetoothDevices: []
readonly property list<BluetoothDevice> availableDevices: []
property bool scanning: false
property bool discoverable: false
// Real Bluetooth Management
Process {
id: bluetoothStatusChecker
command: ["bluetoothctl", "show"] // Use default controller
running: true
property var connectingDevices: ({})
Component.onCompleted: {
refreshBluetoothState()
updateDevices()
}
Connections {
target: Bluetooth
stdout: StdioCollector {
onStreamFinished: {
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller")
root.bluetoothEnabled = text.includes("Powered: yes")
if (root.bluetoothEnabled && root.bluetoothAvailable) {
bluetoothDeviceScanner.running = true
function onDefaultAdapterChanged() {
console.log("BluetoothService: Default adapter changed")
refreshBluetoothState()
updateDevices()
}
}
Connections {
target: Bluetooth.defaultAdapter
function onEnabledChanged() {
refreshBluetoothState()
updateDevices()
}
function onDiscoveringChanged() {
refreshBluetoothState()
updateDevices()
}
}
Connections {
target: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : null
function onModelReset() {
updateDevices()
}
function onItemAdded() {
updateDevices()
}
function onItemRemoved() {
updateDevices()
}
}
Connections {
target: Bluetooth.devices
function onModelReset() {
updateDevices()
}
function onItemAdded() {
updateDevices()
}
function onItemRemoved() {
updateDevices()
}
}
function refreshBluetoothState() {
root.bluetoothAvailable = Bluetooth.defaultAdapter !== null
root.bluetoothEnabled = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.enabled : false
root.scanning = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discovering : false
root.discoverable = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discoverable : false
}
function updateDevices() {
if (!Bluetooth.defaultAdapter) {
clearDeviceList(root.bluetoothDevices)
clearDeviceList(root.availableDevices)
root.bluetoothDevices = []
root.availableDevices = []
return
}
let newPairedDevices = []
let newAvailableDevices = []
let allNativeDevices = []
let adapterDevices = Bluetooth.defaultAdapter.devices
if (adapterDevices.values) {
allNativeDevices = allNativeDevices.concat(adapterDevices.values)
}
if (Bluetooth.devices.values) {
for (let device of Bluetooth.devices.values) {
if (!allNativeDevices.some(d => d.address === device.address)) {
allNativeDevices.push(device)
}
}
}
for (let device of allNativeDevices) {
if (!device) continue
let deviceType = getDeviceType(device.name || device.deviceName, device.icon)
let displayName = device.name || device.deviceName
if (!displayName || displayName.startsWith('/org/bluez') || displayName.includes('hci0') || displayName.length < 2) {
continue
}
if (device.paired) {
let existingDevice = findDeviceInList(root.bluetoothDevices, device.address)
if (existingDevice) {
updateDeviceData(existingDevice, device, deviceType, displayName)
newPairedDevices.push(existingDevice)
} else {
root.bluetoothDevices = []
let newDevice = createBluetoothDevice(device, deviceType, displayName)
newPairedDevices.push(newDevice)
}
}
}
}
Process {
id: bluetoothDeviceScanner
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([0-9A-F:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]]; then info=$(bluetoothctl info $mac); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); battery=$(echo \"$info\" | grep -m1 'Battery Percentage:' | grep -o '[0-9]\\+'); echo \"$mac|$name|$connected|${battery:-}\"; fi; fi; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let devices = []
let lines = text.trim().split('\n')
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 3) {
let mac = parts[0].trim()
let name = parts[1].trim()
let connected = parts[2].trim() === 'yes'
let battery = parts[3] ? parseInt(parts[3]) : -1
// Skip if name is still a technical path
if (name.startsWith('/org/bluez') || name.includes('hci0')) {
continue
}
// Determine device type from name
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
devices.push({
mac: mac,
name: name,
type: type,
connected: connected,
battery: battery
})
}
}
} else {
if (Bluetooth.defaultAdapter.discovering && isDeviceDiscoverable(device)) {
let existingDevice = findDeviceInList(root.availableDevices, device.address)
if (existingDevice) {
updateDeviceData(existingDevice, device, deviceType, displayName)
newAvailableDevices.push(existingDevice)
} else {
let newDevice = createBluetoothDevice(device, deviceType, displayName)
newAvailableDevices.push(newDevice)
}
root.bluetoothDevices = devices
}
}
}
cleanupOldDevices(root.bluetoothDevices, newPairedDevices)
cleanupOldDevices(root.availableDevices, newAvailableDevices)
console.log("BluetoothService: Found", newPairedDevices.length, "paired devices and", newAvailableDevices.length, "available devices")
root.bluetoothDevices = newPairedDevices
root.availableDevices = newAvailableDevices
}
function createBluetoothDevice(nativeDevice, deviceType, displayName) {
return deviceComponent.createObject(root, {
mac: nativeDevice.address,
name: displayName,
type: deviceType,
paired: nativeDevice.paired,
connected: nativeDevice.connected,
battery: nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1,
signalStrength: nativeDevice.connected ? "excellent" : "unknown",
canPair: !nativeDevice.paired,
nativeDevice: nativeDevice,
connecting: false,
connectionFailed: false
})
}
function updateDeviceData(deviceObj, nativeDevice, deviceType, displayName) {
deviceObj.name = displayName
deviceObj.type = deviceType
deviceObj.paired = nativeDevice.paired
// If device connected state changed, clear connecting/failed states
if (deviceObj.connected !== nativeDevice.connected) {
deviceObj.connecting = false
deviceObj.connectionFailed = false
}
deviceObj.connected = nativeDevice.connected
deviceObj.battery = nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1
deviceObj.signalStrength = nativeDevice.connected ? "excellent" : "unknown"
deviceObj.canPair = !nativeDevice.paired
deviceObj.nativeDevice = nativeDevice
}
function findDeviceInList(deviceList, address) {
for (let device of deviceList) {
if (device.mac === address) {
return device
}
}
return null
}
function cleanupOldDevices(oldList, newList) {
for (let oldDevice of oldList) {
if (!newList.includes(oldDevice)) {
oldDevice.destroy()
}
}
}
function scanDevices() {
if (root.bluetoothEnabled && root.bluetoothAvailable) {
bluetoothDeviceScanner.running = true
function clearDeviceList(deviceList) {
for (let device of deviceList) {
device.destroy()
}
}
function isDeviceDiscoverable(device) {
let displayName = device.name || device.deviceName
if (!displayName || displayName.length < 2) return false
if (displayName.startsWith('/org/bluez') || displayName.includes('hci0')) return false
let nameLower = displayName.toLowerCase()
if (nameLower.match(/^[0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}/)) {
return false
}
if (displayName.length < 3) return false
if (nameLower.includes('iphone') || nameLower.includes('ipad') ||
nameLower.includes('airpods') || nameLower.includes('samsung') ||
nameLower.includes('galaxy') || nameLower.includes('pixel') ||
nameLower.includes('headphone') || nameLower.includes('speaker') ||
nameLower.includes('mouse') || nameLower.includes('keyboard') ||
nameLower.includes('watch') || nameLower.includes('buds') ||
nameLower.includes('android')) {
return true
}
return displayName.length >= 4 && !displayName.match(/^[A-Z0-9_-]+$/)
}
function getDeviceType(name, icon) {
if (!name && !icon) return "bluetooth"
let nameLower = (name || "").toLowerCase()
let iconLower = (icon || "").toLowerCase()
if (iconLower.includes("audio") || iconLower.includes("headset") || iconLower.includes("headphone") ||
nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") ||
nameLower.includes("arctis") || nameLower.includes("audio")) return "headset"
else if (iconLower.includes("input-mouse") || nameLower.includes("mouse")) return "mouse"
else if (iconLower.includes("input-keyboard") || nameLower.includes("keyboard")) return "keyboard"
else if (iconLower.includes("phone") || nameLower.includes("phone") || nameLower.includes("iphone") ||
nameLower.includes("samsung") || nameLower.includes("android")) return "phone"
else if (iconLower.includes("watch") || nameLower.includes("watch")) return "watch"
else if (iconLower.includes("audio-speakers") || nameLower.includes("speaker")) return "speaker"
else if (iconLower.includes("video-display") || nameLower.includes("tv") || nameLower.includes("display")) return "tv"
return "bluetooth"
}
function startDiscovery() {
root.scanning = true
// Run comprehensive scan that gets all devices
discoveryScanner.running = true
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) {
Bluetooth.defaultAdapter.discovering = true
updateDevices()
}
}
function stopDiscovery() {
let stopDiscoveryProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "scan", "off"]
running: true
onExited: {
root.scanning = false
}
}
', root)
if (Bluetooth.defaultAdapter) {
Bluetooth.defaultAdapter.discovering = false
updateDevices()
}
}
function pairDevice(mac) {
console.log("Pairing device:", mac)
let pairProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "pair", "' + mac + '"]
running: true
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Pairing successful")
connectDevice("' + mac + '")
} else {
console.warn("Pairing failed with exit code:", exitCode)
}
availableDeviceScanner.running = true
bluetoothDeviceScanner.running = true
}
}
', root)
let device = findDeviceByMac(mac)
if (device) {
device.pair()
}
}
function connectDevice(mac) {
console.log("Connecting to device:", mac)
let connectProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "connect", "' + mac + '"]
running: true
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Connection successful")
} else {
console.warn("Connection failed with exit code:", exitCode)
}
bluetoothDeviceScanner.running = true
}
}
', root)
let device = findDeviceByMac(mac)
if (device) {
device.connect()
}
}
function removeDevice(mac) {
console.log("Removing device:", mac)
let removeProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "remove", "' + mac + '"]
running: true
onExited: {
bluetoothDeviceScanner.running = true
availableDeviceScanner.running = true
}
}
', root)
let device = findDeviceByMac(mac)
if (device) {
device.forget()
}
}
function toggleBluetoothDevice(mac) {
let device = root.bluetoothDevices.find(d => d.mac === mac)
if (device) {
let action = device.connected ? "disconnect" : "connect"
let toggleProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "' + action + '", "' + mac + '"]
running: true
onExited: bluetoothDeviceScanner.running = true
}
', root)
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, mac)
}
if (typedDevice && typedDevice.nativeDevice) {
if (typedDevice.connected) {
console.log("Disconnecting device:", mac)
typedDevice.connecting = false
typedDevice.connectionFailed = false
typedDevice.nativeDevice.connected = false
} else {
console.log("Connecting to device:", mac)
typedDevice.connecting = true
typedDevice.connectionFailed = false
// Set a timeout to handle connection failure
Qt.callLater(() => {
connectionTimeout.deviceMac = mac
connectionTimeout.start()
})
typedDevice.nativeDevice.connected = true
}
}
}
function toggleBluetooth() {
let action = root.bluetoothEnabled ? "off" : "on"
let toggleProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "power", "' + action + '"]
running: true
onExited: bluetoothStatusChecker.running = true
}
', root)
if (Bluetooth.defaultAdapter) {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
}
}
function findDeviceByMac(mac) {
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
if (typedDevice && typedDevice.nativeDevice) {
return typedDevice.nativeDevice
}
typedDevice = findDeviceInList(root.availableDevices, mac)
if (typedDevice && typedDevice.nativeDevice) {
return typedDevice.nativeDevice
}
if (Bluetooth.defaultAdapter) {
let adapterDevices = Bluetooth.defaultAdapter.devices
if (adapterDevices.values) {
for (let device of adapterDevices.values) {
if (device && device.address === mac) {
return device
}
}
}
}
if (Bluetooth.devices.values) {
for (let device of Bluetooth.devices.values) {
if (device && device.address === mac) {
return device
}
}
}
return null
}
Timer {
id: bluetoothMonitorTimer
interval: 5000
running: false; repeat: true
interval: 2000
running: false
repeat: true
onTriggered: {
bluetoothStatusChecker.running = true
if (root.bluetoothEnabled) {
bluetoothDeviceScanner.running = true
// Also refresh paired devices to get current connection status
pairedDeviceChecker.discoveredToMerge = []
pairedDeviceChecker.running = true
}
updateDevices()
}
}
function enableMonitoring(enabled) {
bluetoothMonitorTimer.running = enabled
if (enabled) {
// Immediately update when enabled
bluetoothStatusChecker.running = true
refreshBluetoothState()
updateDevices()
}
}
property var discoveredDevices: []
// Handle discovered devices
function _handleDiscovered(found) {
let discoveredDevices = []
for (let device of found) {
let type = "bluetooth"
let nameLower = device.name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
discoveredDevices.push({
mac: device.mac,
name: device.name,
type: type,
paired: false,
connected: false,
rssi: -70,
signalStrength: "fair",
canPair: true
})
console.log(" -", device.name, "(", device.mac, ")")
Timer {
id: bluetoothStateRefreshTimer
interval: 5000
running: true
repeat: true
onTriggered: {
refreshBluetoothState()
}
// Get paired devices first, then merge with discovered
pairedDeviceChecker.discoveredToMerge = discoveredDevices
pairedDeviceChecker.running = true
}
// Get only currently connected/paired devices that matter
Process {
id: availableDeviceScanner
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); if [[ \"$paired\" == \"yes\" ]] || [[ \"$connected\" == \"yes\" ]]; then echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
Timer {
id: connectionTimeout
interval: 10000 // 10 second timeout
running: false
repeat: false
stdout: StdioCollector {
onStreamFinished: {
let devices = []
if (text.trim()) {
let lines = text.trim().split('\n')
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 4) {
let mac = parts[0].trim()
let name = parts[1].trim()
let paired = parts[2].trim() === 'yes'
let connected = parts[3].trim() === 'yes'
// Skip technical names
if (name.startsWith('/org/bluez') || name.includes('hci0') || name.length < 3) {
continue
}
// Determine device type
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
devices.push({
mac: mac,
name: name,
type: type,
paired: paired,
connected: connected,
rssi: 0,
signalStrength: "unknown",
canPair: false // Already paired
})
}
}
}
property string deviceMac: ""
onTriggered: {
if (deviceMac) {
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
}
root.availableDevices = devices
}
}
}
// Discovery scanner using bluetoothctl --timeout
Process {
id: discoveryScanner
// Discover for 8 s in non-interactive mode, then auto-exit
command: ["bluetoothctl",
"--timeout", "8",
"--monitor", // keeps stdout unbuffered
"scan", "on"]
running: false
stdout: StdioCollector {
onStreamFinished: {
/*
* bluetoothctl prints lines like:
* [NEW] Device 12:34:56:78:9A:BC My-Headphones
*/
const rx = /^\[NEW\] Device ([0-9A-F:]+)\s+(.+)$/i;
const found = text.split('\n')
.filter(l => rx.test(l))
.map(l => {
const [,mac,name] = l.match(rx);
return { mac, name };
});
root._handleDiscovered(found);
}
}
onExited: {
root.scanning = false
}
}
// Get paired devices and merge with discovered ones
Process {
id: pairedDeviceChecker
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ${#name} -gt 3 ]] && [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
running: false
property var discoveredToMerge: []
stdout: StdioCollector {
onStreamFinished: {
// Start with discovered devices (unpaired, available to pair)
let allDevices = [...pairedDeviceChecker.discoveredToMerge]
let seenMacs = new Set(allDevices.map(d => d.mac))
// Add only actually paired devices from bluetoothctl
if (text.trim()) {
let lines = text.trim().split('\n')
if (typedDevice && typedDevice.connecting && !typedDevice.connected) {
console.log("Connection timeout for device:", deviceMac)
typedDevice.connecting = false
typedDevice.connectionFailed = true
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 4) {
let mac = parts[0].trim()
let name = parts[1].trim()
let paired = parts[2].trim() === 'yes'
let connected = parts[3].trim() === 'yes'
// Only include if actually paired
if (!paired) continue
// Check if already in discovered list
if (seenMacs.has(mac)) {
// Update existing device to show it's paired
let existing = allDevices.find(d => d.mac === mac)
if (existing) {
existing.paired = true
existing.connected = connected
existing.canPair = false
}
continue
}
// Add paired device not found during scan
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
allDevices.push({
mac: mac,
name: name,
type: type,
paired: true,
connected: connected,
rssi: -100,
signalStrength: "unknown",
canPair: false
})
}
}
}
// Clear failure state after 3 seconds
Qt.callLater(() => {
clearFailureTimer.deviceMac = deviceMac
clearFailureTimer.start()
})
}
deviceMac = ""
}
}
}
Timer {
id: clearFailureTimer
interval: 3000
running: false
repeat: false
property string deviceMac: ""
onTriggered: {
if (deviceMac) {
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
}
root.availableDevices = allDevices
root.scanning = false
if (typedDevice) {
typedDevice.connectionFailed = false
}
deviceMac = ""
}
}
}
component BluetoothDevice: QtObject {
required property string mac
required property string name
required property string type
required property bool paired
required property bool connected
required property int battery
required property string signalStrength
required property bool canPair
required property var nativeDevice // Reference to native Quickshell device
property bool connecting: false
property bool connectionFailed: false
readonly property string displayName: name
readonly property bool batteryAvailable: battery >= 0
readonly property string connectionStatus: {
if (connecting) return "Connecting..."
if (connectionFailed) return "Connection Failed"
if (connected) return "Connected"
return "Disconnected"
}
}
Component {
id: deviceComponent
BluetoothDevice {}
}
}

View File

@@ -7,15 +7,12 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
// Process list properties
property var processes: []
property bool isUpdating: false
property int processUpdateInterval: 3000
// Performance control - only run when process monitor is actually visible
property bool monitoringEnabled: false
// System information properties
property int totalMemoryKB: 0
property int usedMemoryKB: 0
property int totalSwapKB: 0
@@ -24,28 +21,23 @@ Singleton {
property real totalCpuUsage: 0.0
property bool systemInfoAvailable: false
// Performance history for charts
property var cpuHistory: []
property var memoryHistory: []
property var networkHistory: ({rx: [], tx: []})
property var diskHistory: ({read: [], write: []})
property int historySize: 60 // Keep 60 data points
property int historySize: 60
// Per-core CPU usage
property var perCoreCpuUsage: []
// Network stats
property real networkRxRate: 0 // bytes/sec
property real networkTxRate: 0 // bytes/sec
property real networkRxRate: 0
property real networkTxRate: 0
property var lastNetworkStats: null
// Disk I/O stats
property real diskReadRate: 0 // bytes/sec
property real diskWriteRate: 0 // bytes/sec
property real diskReadRate: 0
property real diskWriteRate: 0
property var lastDiskStats: null
// Sorting options
property string sortBy: "cpu" // "cpu", "memory", "name", "pid"
property string sortBy: "cpu"
property bool sortDescending: true
property int maxProcesses: 20
@@ -53,9 +45,6 @@ Singleton {
console.log("ProcessMonitorService: Starting initialization...")
updateProcessList()
console.log("ProcessMonitorService: Initialization complete")
// Test monitoring disabled - only monitor when explicitly enabled
// testTimer.start()
}
Timer {
@@ -66,7 +55,6 @@ Singleton {
onTriggered: {
console.log("ProcessMonitorService: Starting test monitoring...")
enableMonitoring(true)
// Stop after 8 seconds
stopTestTimer.start()
}
}
@@ -82,7 +70,6 @@ Singleton {
}
}
// System information monitoring
Process {
id: systemInfoProcess
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)]
@@ -104,7 +91,6 @@ Singleton {
}
}
// Network monitoring process
Process {
id: networkStatsProcess
command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"]
@@ -119,7 +105,6 @@ Singleton {
}
}
// Disk I/O monitoring process
Process {
id: diskStatsProcess
command: ["bash", "-c", "cat /proc/diskstats | grep -E ' (sd[a-z]+|nvme[0-9]+n[0-9]+|vd[a-z]+) ' | grep -v 'p[0-9]' | awk '{print $3,$6,$10}'"]
@@ -134,7 +119,6 @@ Singleton {
}
}
// Process monitoring with ps command
Process {
id: processListProcess
command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)]
@@ -146,12 +130,10 @@ Singleton {
const lines = text.trim().split('\n')
const newProcesses = []
// Skip header line
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) continue
// Parse ps output: PID PPID %CPU %MEM RSS COMMAND CMD
const parts = line.split(/\s+/)
if (parts.length >= 7) {
const pid = parseInt(parts[0])
@@ -189,11 +171,10 @@ Singleton {
}
}
// System and process monitoring timer - now conditional
Timer {
id: processTimer
interval: root.processUpdateInterval
running: root.monitoringEnabled // Only run when monitoring is enabled
running: root.monitoringEnabled
repeat: true
onTriggered: {
@@ -206,29 +187,24 @@ Singleton {
}
}
// Public functions
function updateSystemInfo() {
if (!systemInfoProcess.running && root.monitoringEnabled) {
systemInfoProcess.running = true
}
}
// Control functions for enabling/disabling monitoring
function enableMonitoring(enabled) {
console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled")
root.monitoringEnabled = enabled
if (enabled) {
// Clear history when starting
root.cpuHistory = []
root.memoryHistory = []
root.networkHistory = ({rx: [], tx: []})
root.diskHistory = ({read: [], write: []})
// Immediately update when enabled
updateSystemInfo()
updateProcessList()
updateNetworkStats()
updateDiskStats()
// console.log("ProcessMonitorService: Initial data collection started")
}
}
@@ -248,7 +224,6 @@ Singleton {
if (!root.isUpdating && root.monitoringEnabled) {
root.isUpdating = true
// Update sort command based on current sort option
let sortOption = ""
switch (root.sortBy) {
case "cpu":
@@ -307,7 +282,6 @@ Singleton {
}
function getProcessIcon(command) {
// Return appropriate Material Design icon for common processes
const cmd = command.toLowerCase()
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web"
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code"
@@ -315,7 +289,7 @@ Singleton {
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) return "music_note"
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) return "play_circle"
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings"
return "memory" // Default process icon
return "memory"
}
function formatCpuUsage(cpu) {
@@ -444,7 +418,6 @@ Singleton {
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff)
// Convert to KB/s for history
addToHistory(root.networkHistory.rx, root.networkRxRate / 1024)
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
}
@@ -463,7 +436,7 @@ Singleton {
const readSectors = parseInt(parts[1])
const writeSectors = parseInt(parts[2])
if (!isNaN(readSectors) && !isNaN(writeSectors)) {
totalRead += readSectors * 512 // Convert sectors to bytes
totalRead += readSectors * 512
totalWrite += writeSectors * 512
}
}
@@ -474,7 +447,6 @@ Singleton {
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff)
// Convert to MB/s for history
addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024))
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
}

View File

@@ -7,16 +7,13 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
// CPU properties
property real cpuUsage: 0.0
property int cpuCores: 1
property string cpuModel: ""
property real cpuFrequency: 0.0
// Previous CPU stats for accurate calculation
property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0]
// Memory properties
property real memoryUsage: 0.0
property real totalMemory: 0.0
property real usedMemory: 0.0
@@ -25,14 +22,12 @@ Singleton {
property real bufferMemory: 0.0
property real cacheMemory: 0.0
// Temperature properties
property real cpuTemperature: 0.0
property int cpuUpdateInterval: 3000
property int memoryUpdateInterval: 5000
property int temperatureUpdateInterval: 10000
// Performance control
property bool enabledForTopBar: true
property bool enabledForDetailedView: false
@@ -43,7 +38,6 @@ Singleton {
console.log("SystemMonitorService: Initialization complete")
}
// Get CPU information (static)
Process {
id: cpuInfoProcess
command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"]
@@ -69,7 +63,6 @@ Singleton {
}
}
// CPU usage monitoring with accurate calculation
Process {
id: cpuUsageProcess
command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"]
@@ -80,17 +73,14 @@ Singleton {
if (text.trim()) {
const stats = text.trim().split(" ").map(x => parseInt(x))
if (root.prevCpuStats[0] > 0) {
// Calculate differences
let diffs = []
for (let i = 0; i < 8; i++) {
diffs[i] = stats[i] - root.prevCpuStats[i]
}
// Calculate total and idle time
const totalTime = diffs.reduce((a, b) => a + b, 0)
const idleTime = diffs[3] + diffs[4] // idle + iowait
const idleTime = diffs[3] + diffs[4]
// CPU usage percentage
if (totalTime > 0) {
root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100))
}
@@ -107,7 +97,6 @@ Singleton {
}
}
// Memory usage monitoring
Process {
id: memoryUsageProcess
command: ["bash", "-c", "free -m | awk 'NR==2{printf \"%.1f %.1f %.1f %.1f\", $3*100/$2, $2, $3, $7}'"]
@@ -133,7 +122,6 @@ Singleton {
}
}
// CPU frequency monitoring
Process {
id: cpuFrequencyProcess
command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"]
@@ -154,7 +142,6 @@ Singleton {
}
}
// CPU temperature monitoring
Process {
id: temperatureProcess
command: ["bash", "-c", "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then cat /sys/class/thermal/thermal_zone0/temp | awk '{print $1/1000}'; else sensors 2>/dev/null | grep 'Core 0' | awk '{print $3}' | sed 's/+//g;s/°C//g' | head -1; fi"]
@@ -175,7 +162,6 @@ Singleton {
}
}
// CPU monitoring timer
Timer {
id: cpuTimer
interval: root.cpuUpdateInterval
@@ -190,7 +176,6 @@ Singleton {
}
}
// Memory monitoring timer
Timer {
id: memoryTimer
interval: root.memoryUpdateInterval
@@ -204,7 +189,6 @@ Singleton {
}
}
// Temperature monitoring timer
Timer {
id: temperatureTimer
interval: root.temperatureUpdateInterval
@@ -218,7 +202,6 @@ Singleton {
}
}
// Public functions
function getCpuInfo() {
cpuInfoProcess.running = true
}
@@ -243,15 +226,15 @@ Singleton {
}
function getCpuUsageColor() {
if (cpuUsage > 80) return "#e74c3c" // Red
if (cpuUsage > 60) return "#f39c12" // Orange
return "#27ae60" // Green
if (cpuUsage > 80) return "#e74c3c"
if (cpuUsage > 60) return "#f39c12"
return "#27ae60"
}
function getMemoryUsageColor() {
if (memoryUsage > 90) return "#e74c3c" // Red
if (memoryUsage > 75) return "#f39c12" // Orange
return "#3498db" // Blue
if (memoryUsage > 90) return "#e74c3c"
if (memoryUsage > 75) return "#f39c12"
return "#3498db"
}
function formatMemory(mb) {
@@ -262,8 +245,8 @@ Singleton {
}
function getTemperatureColor() {
if (cpuTemperature > 80) return "#e74c3c" // Red
if (cpuTemperature > 65) return "#f39c12" // Orange
return "#27ae60" // Green
if (cpuTemperature > 80) return "#e74c3c"
if (cpuTemperature > 65) return "#f39c12"
return "#27ae60"
}
}

View File

@@ -6,206 +6,445 @@ import Quickshell.Io
import "../../Common"
import "../../Services"
ScrollView {
Item {
id: bluetoothTab
clip: true
// These should be bound from parent
property bool bluetoothEnabled: false
property var bluetoothDevices: []
Column {
width: parent.width
spacing: Theme.spacingL
ScrollView {
anchors.fill: parent
clip: true
// Bluetooth toggle
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent"
border.width: 2
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "bluetooth"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: "Bluetooth"
font.pixelSize: Theme.fontSizeLarge
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.toggleBluetooth()
}
}
}
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
// Bluetooth devices (when enabled)
Column {
width: parent.width
spacing: Theme.spacingM
visible: bluetoothTab.bluetoothEnabled
spacing: Theme.spacingL
Text {
text: "Paired Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
// Real Bluetooth devices
Repeater {
model: bluetoothTab.bluetoothDevices
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent"
border.width: 2
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
Text {
text: "bluetooth"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
switch (modelData.type) {
case "headset": return "headset"
case "mouse": return "mouse"
case "keyboard": return "keyboard"
case "phone": return "smartphone"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
text: "Bluetooth"
font.pixelSize: Theme.fontSizeLarge
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: modelData.connected ? "Connected" : "Disconnected"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
Text {
text: modelData.battery >= 0 ? "• " + modelData.battery + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.battery >= 0
}
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.toggleBluetoothDevice(modelData.mac)
Text {
text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.toggleBluetooth()
}
}
}
}
// Available devices for pairing (when enabled)
Column {
width: parent.width
spacing: Theme.spacingM
visible: bluetoothTab.bluetoothEnabled
Row {
Column {
width: parent.width
spacing: Theme.spacingM
visible: bluetoothTab.bluetoothEnabled
Text {
text: "Available Devices"
text: "Paired Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: 1; height: 1 }
Repeater {
model: bluetoothTab.bluetoothDevices
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
switch (modelData.type) {
case "headset": return "headset"
case "mouse": return "mouse"
case "keyboard": return "keyboard"
case "phone": return "smartphone"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: {
if (modelData.connecting) return Theme.primary
if (modelData.connected) return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
opacity: modelData.connecting ? 0.6 : 1.0
Behavior on opacity {
SequentialAnimation {
running: modelData.connecting
loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 0.3; duration: 800 }
NumberAnimation { from: 0.3; to: 1.0; duration: 800 }
}
}
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: modelData.connectionStatus
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.connecting) return Theme.primary
if (modelData.connectionFailed) return Theme.error
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
Text {
text: modelData.battery >= 0 ? "• " + modelData.battery + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.battery >= 0
}
}
}
}
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Text {
text: "more_vert"
font.family: Theme.iconFont
font.weight: Theme.iconFontWeight
font.pixelSize: Theme.iconSize
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: !modelData.connecting
enabled: !modelData.connecting
cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: {
if (!modelData.connecting) {
bluetoothContextMenuWindow.deviceData = modelData
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height)
bluetoothContextMenuWindow.show(localPos.x, localPos.y)
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40 // Don't overlap with menu button
hoverEnabled: !modelData.connecting
enabled: !modelData.connecting
cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: {
if (!modelData.connecting) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: bluetoothTab.bluetoothEnabled
Rectangle {
width: Math.max(100, scanText.contentWidth + Theme.spacingM * 2)
height: 32
radius: Theme.cornerRadius
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Theme.primary
border.width: 1
Row {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Available Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: 1; height: 1 }
Rectangle {
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
height: 36
radius: Theme.cornerRadius
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: BluetoothService.scanning ? "stop" : "bluetooth_searching"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: scanText
text: BluetoothService.scanning ? "Stop Scanning" : "Start Scanning"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.scanning) {
BluetoothService.stopDiscovery()
} else {
BluetoothService.startDiscovery()
}
}
}
}
}
Repeater {
model: BluetoothService.availableDevices
Rectangle {
width: parent.width
height: 70
radius: Theme.cornerRadius
color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent")
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
switch (modelData.type) {
case "headset": return "headset"
case "mouse": return "mouse"
case "keyboard": return "keyboard"
case "phone": return "smartphone"
case "watch": return "watch"
case "speaker": return "speaker"
case "tv": return "tv"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.paired ? Theme.secondary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.paired ? Theme.secondary : Theme.surfaceText
font.weight: modelData.paired ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: {
if (modelData.paired && modelData.connected) return "Connected"
if (modelData.paired) return "Paired"
return "Signal: " + modelData.signalStrength
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
Text {
text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.rssi !== 0
}
}
}
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadiusSmall
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: Theme.primary
border.width: 1
visible: modelData.canPair || modelData.paired
Text {
anchors.centerIn: parent
text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
if (modelData.connected) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.connectDevice(modelData.mac)
}
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90 // Don't overlap with action button
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.scanning && BluetoothService.availableDevices.length === 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Text {
text: BluetoothService.scanning ? "search" : "bluetooth_searching"
text: "sync"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
font.pixelSize: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: BluetoothService.scanning
running: true
loops: Animation.Infinite
from: 0
to: 360
@@ -214,169 +453,237 @@ ScrollView {
}
Text {
id: scanText
text: BluetoothService.scanning ? "Scanning..." : "Scan"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
text: "Scanning for devices..."
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !BluetoothService.scanning
onClicked: {
BluetoothService.startDiscovery()
Text {
text: "Make sure your device is in pairing mode"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Text {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
}
}
Rectangle {
id: bluetoothContextMenuWindow
property var deviceData: null
property bool menuVisible: false
visible: false
width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
opacity: menuVisible ? 1.0 : 0.0
scale: menuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "link_off" : "link"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.toggleBluetoothDevice(bluetoothContextMenuWindow.deviceData.mac)
}
bluetoothContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Available devices list
Repeater {
model: BluetoothService.availableDevices
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 70
radius: Theme.cornerRadius
color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent")
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
switch (modelData.type) {
case "headset": return "headset"
case "mouse": return "mouse"
case "keyboard": return "keyboard"
case "phone": return "smartphone"
case "watch": return "watch"
case "speaker": return "speaker"
case "tv": return "tv"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.paired ? Theme.secondary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.paired ? Theme.secondary : Theme.surfaceText
font.weight: modelData.paired ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: {
if (modelData.paired && modelData.connected) return "Connected"
if (modelData.paired) return "Paired"
return "Signal: " + modelData.signalStrength
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
Text {
text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.rssi !== 0
}
}
}
}
// Action button on the right
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadiusSmall
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: Theme.primary
border.width: 1
visible: modelData.canPair || modelData.paired
Text {
anchors.centerIn: parent
text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
if (modelData.connected) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.connectDevice(modelData.mac)
}
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90 // Don't overlap with action button
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
// No devices message
Text {
text: "No devices found. Put your device in pairing mode and click Scan."
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning
wrapMode: Text.WordWrap
Rectangle {
width: parent.width
horizontalAlignment: Text.AlignHCenter
height: 32
radius: Theme.cornerRadiusSmall
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: "delete"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.removeDevice(bluetoothContextMenuWindow.deviceData.mac)
}
bluetoothContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
function show(x, y) {
const menuWidth = 160
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x - menuWidth / 2
let finalY = y
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth))
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight))
bluetoothContextMenuWindow.x = finalX
bluetoothContextMenuWindow.y = finalY
bluetoothContextMenuWindow.visible = true
bluetoothContextMenuWindow.menuVisible = true
}
function hide() {
bluetoothContextMenuWindow.menuVisible = false
Qt.callLater(() => { bluetoothContextMenuWindow.visible = false })
}
}
MouseArea {
anchors.fill: parent
visible: bluetoothContextMenuWindow.visible
onClicked: {
bluetoothContextMenuWindow.hide()
}
MouseArea {
x: bluetoothContextMenuWindow.x
y: bluetoothContextMenuWindow.y
width: bluetoothContextMenuWindow.width
height: bluetoothContextMenuWindow.height
onClicked: {
}
}
}

View File

@@ -334,7 +334,7 @@ PanelWindow {
topBar.shellRoot.controlCenterVisible = !topBar.shellRoot.controlCenterVisible
if (topBar.shellRoot.controlCenterVisible) {
WifiService.scanWifi()
BluetoothService.scanDevices()
// Bluetooth devices are automatically updated via signals
}
}
}