fedora
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.Power
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Variants {
|
||||
id: root
|
||||
model: Quickshell.screens
|
||||
|
||||
// Direct binding to registry's widgets property for reactivity
|
||||
readonly property var registeredWidgets: DesktopWidgetRegistry.widgets
|
||||
|
||||
// Force reload counter - incremented when plugin widget registry changes
|
||||
property int pluginReloadCounter: 0
|
||||
|
||||
Connections {
|
||||
target: DesktopWidgetRegistry
|
||||
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
root.pluginReloadCounter++;
|
||||
Logger.d("DesktopWidgets", "Plugin widget registry updated, reload counter:", root.pluginReloadCounter);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Loader {
|
||||
id: screenLoader
|
||||
required property ShellScreen modelData
|
||||
|
||||
// Reactive property for widgets on this specific screen
|
||||
// Returns a fresh array whenever Settings changes
|
||||
property var screenWidgets: {
|
||||
if (!modelData || !modelData.name) {
|
||||
return [];
|
||||
}
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
for (var i = 0; i < monitorWidgets.length; i++) {
|
||||
if (monitorWidgets[i].name === modelData.name) {
|
||||
return monitorWidgets[i].widgets || [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Only create PanelWindow if enabled AND (screen has widgets OR in edit mode)
|
||||
// During compositor overview, show widgets only when overviewEnabled is true.
|
||||
active: modelData && Settings.data.desktopWidgets.enabled && (screenWidgets.length > 0 || DesktopWidgetRegistry.editMode) && (!CompositorService.overviewActive || Settings.data.desktopWidgets.overviewEnabled) && (!PowerProfileService.noctaliaPerformanceMode || !Settings.data.noctaliaPerformance.disableDesktopWidgets) && !PanelService.lockScreen?.active
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: window
|
||||
color: "transparent"
|
||||
screen: screenLoader.modelData
|
||||
mask: DesktopWidgetRegistry.editMode ? null : widgetsMask
|
||||
|
||||
// Dynamic mask: combine clickable regions for each loaded widget
|
||||
property var _maskRegions: []
|
||||
|
||||
Component {
|
||||
id: maskRegionComponent
|
||||
Region {}
|
||||
}
|
||||
|
||||
Region {
|
||||
id: widgetsMask
|
||||
regions: window._maskRegions
|
||||
}
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Bottom
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "noctalia-desktop-widgets-" + (screen?.name || "unknown")
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
right: true
|
||||
left: true
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.d("DesktopWidgets", "Created panel window for", screen?.name);
|
||||
}
|
||||
|
||||
// Add a new widget to the current screen
|
||||
function addWidgetToCurrentScreen(widgetId) {
|
||||
var monitorName = window.screen.name;
|
||||
var newWidget = {
|
||||
"id": widgetId
|
||||
};
|
||||
|
||||
// Load default metadata if available
|
||||
var metadata = DesktopWidgetRegistry.widgetMetadata[widgetId];
|
||||
if (metadata) {
|
||||
Object.keys(metadata).forEach(function (key) {
|
||||
newWidget[key] = metadata[key];
|
||||
});
|
||||
}
|
||||
|
||||
// Place at screen center
|
||||
newWidget.x = (window.screen.width / 2) - 100;
|
||||
newWidget.y = (window.screen.height / 2) - 100;
|
||||
newWidget.scale = 1.0;
|
||||
|
||||
// Get current widgets and add new one
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
var newMonitorWidgets = monitorWidgets.slice();
|
||||
var found = false;
|
||||
|
||||
for (var i = 0; i < newMonitorWidgets.length; i++) {
|
||||
if (newMonitorWidgets[i].name === monitorName) {
|
||||
var widgets = (newMonitorWidgets[i].widgets || []).slice();
|
||||
widgets.push(newWidget);
|
||||
newMonitorWidgets[i] = {
|
||||
"name": monitorName,
|
||||
"widgets": widgets
|
||||
};
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
newMonitorWidgets.push({
|
||||
"name": monitorName,
|
||||
"widgets": [newWidget]
|
||||
});
|
||||
}
|
||||
|
||||
Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets;
|
||||
Logger.i("DesktopWidgets", "Added widget", widgetId, "to", monitorName);
|
||||
}
|
||||
|
||||
Item {
|
||||
id: widgetsContainer
|
||||
anchors.fill: parent
|
||||
|
||||
// Visual grid overlay - shown when grid snap is enabled in edit mode
|
||||
// Using Loader to properly unload Canvas when not needed
|
||||
Loader {
|
||||
id: gridOverlayLoader
|
||||
active: DesktopWidgetRegistry.editMode && Settings.data.desktopWidgets.enabled && Settings.data.desktopWidgets.gridSnap
|
||||
anchors.fill: parent
|
||||
z: -1 // Behind widgets but above background
|
||||
asynchronous: false
|
||||
|
||||
sourceComponent: Canvas {
|
||||
id: gridOverlay
|
||||
anchors.fill: parent
|
||||
opacity: 0.3
|
||||
|
||||
// Grid size calculated based on screen resolution - matches DraggableDesktopWidget
|
||||
// Ensures grid lines pass through the screen center on both axes
|
||||
readonly property int gridSize: {
|
||||
if (!window.screen)
|
||||
return 30; // Fallback
|
||||
var baseSize = Math.round(window.screen.width * 0.015);
|
||||
baseSize = Math.max(20, Math.min(60, baseSize));
|
||||
|
||||
// Calculate center coordinates
|
||||
var centerX = window.screen.width / 2;
|
||||
var centerY = window.screen.height / 2;
|
||||
|
||||
// Find a grid size that divides evenly into both center coordinates
|
||||
// This ensures a grid line crosses through the center on both axes
|
||||
var bestSize = baseSize;
|
||||
var bestDistance = Infinity;
|
||||
|
||||
// Try values around baseSize to find one that divides evenly into both centers
|
||||
for (var offset = -10; offset <= 10; offset++) {
|
||||
var candidate = baseSize + offset;
|
||||
if (candidate < 20 || candidate > 60)
|
||||
continue;
|
||||
|
||||
// Check if this size divides evenly into both center coordinates
|
||||
var remainderX = centerX % candidate;
|
||||
var remainderY = centerY % candidate;
|
||||
|
||||
// If both remainders are 0, this is perfect - center is on grid lines
|
||||
if (remainderX === 0 && remainderY === 0) {
|
||||
return candidate; // Perfect match, use it immediately
|
||||
}
|
||||
|
||||
// Otherwise, find the closest to perfect alignment
|
||||
var distance = Math.abs(remainderX) + Math.abs(remainderY);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestSize = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a perfect match, it would have returned already
|
||||
// Otherwise, try to find a divisor of both centerX and centerY
|
||||
// that's close to our best size
|
||||
var gcd = function (a, b) {
|
||||
while (b !== 0) {
|
||||
var temp = b;
|
||||
b = a % b;
|
||||
a = temp;
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
// Find common divisors of centerX and centerY
|
||||
var centerGcd = gcd(Math.round(centerX), Math.round(centerY));
|
||||
if (centerGcd > 0) {
|
||||
// Find a divisor of centerGcd that's close to bestSize
|
||||
for (var divisor = Math.floor(centerGcd / 60); divisor <= Math.ceil(centerGcd / 20); divisor++) {
|
||||
if (centerGcd % divisor !== 0)
|
||||
continue;
|
||||
var candidate = centerGcd / divisor;
|
||||
if (candidate >= 20 && candidate <= 60) {
|
||||
if (Math.abs(candidate - baseSize) < Math.abs(bestSize - baseSize)) {
|
||||
bestSize = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestSize;
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
ctx.strokeStyle = Color.mPrimary;
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// Draw vertical lines
|
||||
for (let x = 0; x <= width; x += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw horizontal lines
|
||||
for (let y = 0; y <= height; y += gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Repaint when size changes
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
Component.onCompleted: {
|
||||
requestPaint();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.desktopWidgets
|
||||
function onGridSnapChanged() {
|
||||
if (gridOverlayLoader.active) {
|
||||
gridOverlay.requestPaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopWidgetRegistry
|
||||
function onEditModeChanged() {
|
||||
if (gridOverlayLoader.active) {
|
||||
gridOverlay.requestPaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load widgets dynamically from per-monitor array
|
||||
Repeater {
|
||||
model: screenLoader.screenWidgets
|
||||
|
||||
delegate: Loader {
|
||||
id: widgetLoader
|
||||
// Bind to registeredWidgets and pluginReloadCounter to re-evaluate when plugins register/unregister
|
||||
active: (modelData.id in root.registeredWidgets) && (root.pluginReloadCounter >= 0)
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
property var _maskRegion: null
|
||||
readonly property bool _isPlugin: DesktopWidgetRegistry.isPluginWidget(modelData.id)
|
||||
|
||||
// All widgets use setSource() so that screen, widgetData, and
|
||||
// widgetIndex are set as initial properties, available during
|
||||
// Component.onCompleted. This prevents registration-key
|
||||
// mismatches in widgets that build IDs from screen.name.
|
||||
Component.onCompleted: _loadWidget()
|
||||
|
||||
onActiveChanged: {
|
||||
if (active)
|
||||
_loadWidget();
|
||||
}
|
||||
|
||||
function _loadWidget() {
|
||||
var widgetId = modelData.id;
|
||||
var comp = root.registeredWidgets[widgetId];
|
||||
if (!comp)
|
||||
return;
|
||||
|
||||
var props = {
|
||||
"screen": window.screen,
|
||||
"widgetData": modelData,
|
||||
"widgetIndex": index
|
||||
};
|
||||
|
||||
if (_isPlugin) {
|
||||
var pluginId = widgetId.replace("plugin:", "");
|
||||
var api = PluginService.getPluginAPI(pluginId);
|
||||
if (api)
|
||||
props.pluginApi = api;
|
||||
setSource(comp.url, props);
|
||||
} else {
|
||||
// Core widgets: use explicit URL (inline Component.url
|
||||
// returns the registry file, not the widget file)
|
||||
var url = DesktopWidgetRegistry.widgetUrls[widgetId];
|
||||
if (url)
|
||||
setSource(url, props);
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
item.parent = widgetsContainer;
|
||||
|
||||
// Create mask region so this widget receives mouse input
|
||||
_maskRegion = maskRegionComponent.createObject(window);
|
||||
_maskRegion.item = item;
|
||||
var newRegions = window._maskRegions.slice();
|
||||
newRegions.push(_maskRegion);
|
||||
window._maskRegions = newRegions;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up mask region when widget unloads
|
||||
onItemChanged: {
|
||||
if (!item && _maskRegion) {
|
||||
var region = _maskRegion;
|
||||
_maskRegion = null;
|
||||
window._maskRegions = window._maskRegions.filter(function (r) {
|
||||
return r !== region;
|
||||
});
|
||||
region.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edit mode controls panel
|
||||
Rectangle {
|
||||
id: editModeControlsPanel
|
||||
visible: DesktopWidgetRegistry.editMode && Settings.data.desktopWidgets.enabled
|
||||
|
||||
readonly property string barPos: Settings.getBarPositionForScreen(window.screen?.name)
|
||||
readonly property bool barFloating: Settings.data.bar.barType === "floating"
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(window.screen?.name)
|
||||
|
||||
readonly property int barOffsetTop: {
|
||||
if (barPos !== "top")
|
||||
return Style.marginM;
|
||||
const floatMarginV = barFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0;
|
||||
return barHeight + floatMarginV + Style.marginM;
|
||||
}
|
||||
readonly property int barOffsetRight: {
|
||||
if (barPos !== "right")
|
||||
return Style.marginM;
|
||||
const floatMarginH = barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0;
|
||||
return barHeight + floatMarginH + Style.marginM;
|
||||
}
|
||||
|
||||
// Internal state for drag tracking (session-only, resets on restart)
|
||||
QtObject {
|
||||
id: panelInternal
|
||||
property bool isDragging: false
|
||||
property real dragOffsetX: 0
|
||||
property real dragOffsetY: 0
|
||||
// Default position: top-right corner accounting for bar
|
||||
property real baseX: widgetsContainer.width - editModeControlsPanel.width - editModeControlsPanel.barOffsetRight
|
||||
property real baseY: editModeControlsPanel.barOffsetTop
|
||||
}
|
||||
|
||||
// Reset position when bar position changes
|
||||
Connections {
|
||||
target: Settings.data.bar
|
||||
function onPositionChanged() {
|
||||
panelInternal.baseX = widgetsContainer.width - editModeControlsPanel.width - editModeControlsPanel.barOffsetRight;
|
||||
panelInternal.baseY = editModeControlsPanel.barOffsetTop;
|
||||
}
|
||||
}
|
||||
|
||||
x: panelInternal.isDragging ? panelInternal.dragOffsetX : panelInternal.baseX
|
||||
y: panelInternal.isDragging ? panelInternal.dragOffsetY : panelInternal.baseY
|
||||
|
||||
width: controlsLayout.implicitWidth + Style.margin2XL
|
||||
height: controlsLayout.implicitHeight + Style.margin2XL
|
||||
|
||||
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.85)
|
||||
radius: Style.radiusL
|
||||
border {
|
||||
width: Style.borderS
|
||||
color: Color.mOutline
|
||||
}
|
||||
z: 9999
|
||||
|
||||
// Drag area for relocating the panel
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
cursorShape: panelInternal.isDragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
|
||||
property point pressPos: Qt.point(0, 0)
|
||||
|
||||
onPressed: mouse => {
|
||||
pressPos = mapToItem(widgetsContainer, mouse.x, mouse.y);
|
||||
panelInternal.dragOffsetX = editModeControlsPanel.x;
|
||||
panelInternal.dragOffsetY = editModeControlsPanel.y;
|
||||
panelInternal.isDragging = true;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (panelInternal.isDragging && pressed) {
|
||||
var currentPos = mapToItem(widgetsContainer, mouse.x, mouse.y);
|
||||
var deltaX = currentPos.x - pressPos.x;
|
||||
var deltaY = currentPos.y - pressPos.y;
|
||||
|
||||
var newX = panelInternal.baseX + deltaX;
|
||||
var newY = panelInternal.baseY + deltaY;
|
||||
|
||||
// Boundary clamping
|
||||
newX = Math.max(0, Math.min(newX, widgetsContainer.width - editModeControlsPanel.width));
|
||||
newY = Math.max(0, Math.min(newY, widgetsContainer.height - editModeControlsPanel.height));
|
||||
|
||||
panelInternal.dragOffsetX = newX;
|
||||
panelInternal.dragOffsetY = newY;
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (panelInternal.isDragging) {
|
||||
panelInternal.baseX = panelInternal.dragOffsetX;
|
||||
panelInternal.baseY = panelInternal.dragOffsetY;
|
||||
panelInternal.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
onCanceled: {
|
||||
panelInternal.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: controlsLayout
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Style.marginXL
|
||||
}
|
||||
spacing: Style.marginL
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: Style.marginS
|
||||
|
||||
NIconButton {
|
||||
id: addWidgetButton
|
||||
icon: "layout-grid-add"
|
||||
tooltipText: I18n.tr("tooltips.add-widget")
|
||||
onClicked: {
|
||||
var popupMenuWindow = PanelService.getPopupMenuWindow(window.screen);
|
||||
if (popupMenuWindow) {
|
||||
// Build menu items from registry
|
||||
var items = [];
|
||||
var widgets = DesktopWidgetRegistry.widgets;
|
||||
for (var id in widgets) {
|
||||
items.push({
|
||||
action: id,
|
||||
text: DesktopWidgetRegistry.getWidgetDisplayName(id),
|
||||
icon: "layout-grid-add"
|
||||
});
|
||||
}
|
||||
var globalPos = addWidgetButton.mapToItem(null, 0, addWidgetButton.height + Style.marginS);
|
||||
popupMenuWindow.showDynamicContextMenu(items, globalPos.x, globalPos.y, function (widgetId) {
|
||||
addWidgetToCurrentScreen(widgetId);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "grid-3x3"
|
||||
visible: Settings.data.desktopWidgets.gridSnap
|
||||
tooltipText: I18n.tr("panels.desktop-widgets.edit-mode-grid-snap-scale-label")
|
||||
colorBg: Settings.data.desktopWidgets.gridSnapScale ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: Settings.data.desktopWidgets.gridSnapScale ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: Settings.data.desktopWidgets.gridSnapScale = !Settings.data.desktopWidgets.gridSnapScale
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "grid-4x4"
|
||||
tooltipText: I18n.tr("panels.desktop-widgets.edit-mode-grid-snap-label")
|
||||
colorBg: Settings.data.desktopWidgets.gridSnap ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: Settings.data.desktopWidgets.gridSnap ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: Settings.data.desktopWidgets.gridSnap = !Settings.data.desktopWidgets.gridSnap
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "settings"
|
||||
tooltipText: I18n.tr("actions.open-settings")
|
||||
onClicked: {
|
||||
SettingsPanelService.toggle(SettingsPanel.Tab.DesktopWidgets, -1, screenLoader.modelData);
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("panels.desktop-widgets.edit-mode-exit-button")
|
||||
icon: "logout"
|
||||
outlined: false
|
||||
fontSize: Style.fontSizeS
|
||||
iconSize: Style.fontSizeM
|
||||
onClicked: DesktopWidgetRegistry.editMode = false
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.maximumWidth: 300 * Style.uiScaleRatio
|
||||
text: I18n.tr("panels.desktop-widgets.edit-mode-controls-explanation")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignRight
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+634
@@ -0,0 +1,634 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property var widgetData: null
|
||||
property int widgetIndex: -1
|
||||
|
||||
property real defaultX: 100
|
||||
property real defaultY: 100
|
||||
|
||||
default property alias content: contentContainer.data
|
||||
|
||||
readonly property bool isDragging: internal.isDragging
|
||||
readonly property bool isScaling: internal.isScaling
|
||||
|
||||
// All Desktop widgets have these settings, but fallback just in case
|
||||
readonly property var _metadata: widgetData?.id ? DesktopWidgetRegistry.widgetMetadata[widgetData.id] : null
|
||||
property bool showBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : (_metadata?.showBackground ?? true)
|
||||
property bool roundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : (_metadata?.roundedCorners ?? true)
|
||||
|
||||
property real widgetScale: 1.0
|
||||
property real minScale: 0.5
|
||||
property real maxScale: 5.0
|
||||
|
||||
readonly property real scaleSensitivity: 0.0015
|
||||
readonly property real scaleUpdateThreshold: 0.015
|
||||
readonly property real cornerScaleSensitivity: 0.0003 // Much lower sensitivity for corner handles
|
||||
|
||||
// Grid size ensures lines pass through screen center on both axes
|
||||
readonly property int gridSize: {
|
||||
if (!screen)
|
||||
return 30;
|
||||
var baseSize = Math.round(screen.width * 0.015);
|
||||
baseSize = Math.max(20, Math.min(60, baseSize));
|
||||
|
||||
var centerX = screen.width / 2;
|
||||
var centerY = screen.height / 2;
|
||||
var bestSize = baseSize;
|
||||
var bestDistance = Infinity;
|
||||
|
||||
for (var offset = -10; offset <= 10; offset++) {
|
||||
var candidate = baseSize + offset;
|
||||
if (candidate < 20 || candidate > 60)
|
||||
continue;
|
||||
|
||||
var remainderX = centerX % candidate;
|
||||
var remainderY = centerY % candidate;
|
||||
|
||||
if (remainderX === 0 && remainderY === 0) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var distance = Math.abs(remainderX) + Math.abs(remainderY);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestSize = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
var gcd = function (a, b) {
|
||||
while (b !== 0) {
|
||||
var temp = b;
|
||||
b = a % b;
|
||||
a = temp;
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
var centerGcd = gcd(Math.round(centerX), Math.round(centerY));
|
||||
if (centerGcd > 0) {
|
||||
for (var divisor = Math.floor(centerGcd / 60); divisor <= Math.ceil(centerGcd / 20); divisor++) {
|
||||
if (centerGcd % divisor !== 0)
|
||||
continue;
|
||||
var candidate = centerGcd / divisor;
|
||||
if (candidate >= 20 && candidate <= 60) {
|
||||
if (Math.abs(candidate - baseSize) < Math.abs(bestSize - baseSize)) {
|
||||
bestSize = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestSize;
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: internal
|
||||
property bool isDragging: false
|
||||
property bool isScaling: false
|
||||
property real dragOffsetX: 0
|
||||
property real dragOffsetY: 0
|
||||
property real baseX: (root.widgetData && root.widgetData.x !== undefined) ? root.widgetData.x : root.defaultX
|
||||
property real baseY: (root.widgetData && root.widgetData.y !== undefined) ? root.widgetData.y : root.defaultY
|
||||
property real initialWidth: 0
|
||||
property real initialHeight: 0
|
||||
property point initialMousePos: Qt.point(0, 0)
|
||||
property real initialScale: 1.0
|
||||
property real lastScale: 1.0
|
||||
// Locks operation type to prevent switching between drag/scale mid-operation
|
||||
property string operationType: "" // "drag" or "scale" or ""
|
||||
}
|
||||
|
||||
function snapToGrid(coord) {
|
||||
if (!Settings.data.desktopWidgets.gridSnap) {
|
||||
return coord;
|
||||
}
|
||||
return Math.round(coord / root.gridSize) * root.gridSize;
|
||||
}
|
||||
|
||||
function snapScaleToGrid(scale) {
|
||||
if (!Settings.data.desktopWidgets.gridSnap || !Settings.data.desktopWidgets.gridSnapScale) {
|
||||
return scale;
|
||||
}
|
||||
|
||||
// Get widget's base width
|
||||
var initialWidth = internal.initialWidth;
|
||||
var initialScale = internal.initialScale;
|
||||
if (initialWidth <= 0 || initialScale <= 0) {
|
||||
return scale;
|
||||
}
|
||||
|
||||
// Since initialWidth = baseWidth * initialScale
|
||||
var baseWidth = initialWidth / initialScale;
|
||||
|
||||
// Snap the resulting width with the scale
|
||||
var resultingWidth = baseWidth * scale;
|
||||
var snappedWidth = root.snapToGrid(resultingWidth);
|
||||
|
||||
// Check that the snappedWidth isn't smaller than one grid size
|
||||
if (snappedWidth < root.gridSize) {
|
||||
snappedWidth = root.gridSize;
|
||||
}
|
||||
|
||||
// Return the ratio of the snappedWidth and the baseWidth, which is the new snapped scale
|
||||
var snappedScale = snappedWidth / baseWidth;
|
||||
return Math.max(minScale, Math.min(maxScale, snappedScale));
|
||||
}
|
||||
|
||||
function updateWidgetData(properties) {
|
||||
if (widgetIndex < 0 || !screen || !screen.name) {
|
||||
return;
|
||||
}
|
||||
DesktopWidgetRegistry.updateWidgetData(screen.name, widgetIndex, properties);
|
||||
}
|
||||
|
||||
function removeWidget() {
|
||||
if (widgetIndex < 0 || !screen || !screen.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
var newMonitorWidgets = monitorWidgets.slice();
|
||||
|
||||
for (var i = 0; i < newMonitorWidgets.length; i++) {
|
||||
if (newMonitorWidgets[i].name === screen.name) {
|
||||
var widgets = (newMonitorWidgets[i].widgets || []).slice();
|
||||
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
|
||||
widgets.splice(widgetIndex, 1);
|
||||
newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], {
|
||||
"widgets": widgets
|
||||
});
|
||||
Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function raiseToTop() {
|
||||
if (widgetIndex < 0 || !screen || !screen.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
var newMonitorWidgets = monitorWidgets.slice();
|
||||
|
||||
for (var i = 0; i < newMonitorWidgets.length; i++) {
|
||||
if (newMonitorWidgets[i].name === screen.name) {
|
||||
var widgets = (newMonitorWidgets[i].widgets || []).slice();
|
||||
if (widgetIndex < widgets.length && widgetIndex < widgets.length - 1) {
|
||||
var widget = widgets.splice(widgetIndex, 1)[0];
|
||||
widgets.push(widget);
|
||||
newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], {
|
||||
"widgets": widgets
|
||||
});
|
||||
Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lowerToBottom() {
|
||||
if (widgetIndex < 0 || !screen || !screen.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
|
||||
var newMonitorWidgets = monitorWidgets.slice();
|
||||
|
||||
for (var i = 0; i < newMonitorWidgets.length; i++) {
|
||||
if (newMonitorWidgets[i].name === screen.name) {
|
||||
var widgets = (newMonitorWidgets[i].widgets || []).slice();
|
||||
if (widgetIndex < widgets.length && widgetIndex > 0) {
|
||||
var widget = widgets.splice(widgetIndex, 1)[0];
|
||||
widgets.unshift(widget);
|
||||
newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], {
|
||||
"widgets": widgets
|
||||
});
|
||||
Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openWidgetSettings() {
|
||||
DesktopWidgetRegistry.openWidgetSettings(screen, widgetIndex, widgetData.id, widgetData);
|
||||
}
|
||||
|
||||
function handleContextMenuAction(action) {
|
||||
if (action === "widget-settings") {
|
||||
// Don't close - openWidgetSettings will use the popup window for the dialog
|
||||
root.openWidgetSettings();
|
||||
return true; // Signal that we're handling close ourselves
|
||||
} else if (action === "reset") {
|
||||
// Reset scale and position to defaults
|
||||
root.widgetScale = 1.0;
|
||||
internal.baseX = root.defaultX;
|
||||
internal.baseY = root.defaultY;
|
||||
root.updateWidgetData({
|
||||
"scale": 1.0,
|
||||
"x": Math.round(root.defaultX),
|
||||
"y": Math.round(root.defaultY)
|
||||
});
|
||||
return false;
|
||||
} else if (action === "raise-to-top") {
|
||||
root.raiseToTop();
|
||||
return false;
|
||||
} else if (action === "lower-to-bottom") {
|
||||
root.lowerToBottom();
|
||||
return false;
|
||||
} else if (action === "delete") {
|
||||
root.removeWidget();
|
||||
return false; // Let caller close the popup
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
x: Math.round(internal.isDragging ? internal.dragOffsetX : internal.baseX)
|
||||
y: Math.round(internal.isDragging ? internal.dragOffsetY : internal.baseY)
|
||||
|
||||
// Note: We no longer use transform-based scaling (scale property)
|
||||
// Instead, child widgets multiply their dimensions by widgetScale
|
||||
// This prevents blurry text at fractional scale values
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize scale from widgetData when component is first created
|
||||
if (widgetData && widgetData.scale !== undefined) {
|
||||
widgetScale = widgetData.scale;
|
||||
}
|
||||
}
|
||||
|
||||
onWidgetDataChanged: {
|
||||
if (!internal.isDragging && !internal.isScaling) {
|
||||
internal.baseX = (widgetData && widgetData.x !== undefined) ? widgetData.x : defaultX;
|
||||
internal.baseY = (widgetData && widgetData.y !== undefined) ? widgetData.y : defaultY;
|
||||
if (widgetData && widgetData.scale !== undefined) {
|
||||
widgetScale = widgetData.scale;
|
||||
} else if (widgetData) {
|
||||
// If widgetData exists but scale is not set, default to 1.0
|
||||
widgetScale = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: decorationRect
|
||||
anchors.fill: parent
|
||||
anchors.margins: -outlineMargin
|
||||
color: DesktopWidgetRegistry.editMode ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.1) : "transparent"
|
||||
border.color: (DesktopWidgetRegistry.editMode || internal.isDragging) ? (internal.isDragging ? Color.mOutline : Color.mPrimary) : "transparent"
|
||||
border.width: DesktopWidgetRegistry.editMode ? 3 : 0
|
||||
radius: Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2)
|
||||
z: -1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: container
|
||||
anchors.fill: parent
|
||||
radius: root.roundedCorners ? Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2) : 0
|
||||
color: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity)
|
||||
border {
|
||||
width: 1
|
||||
color: Qt.alpha(Color.mOutline, 0.12)
|
||||
}
|
||||
clip: true
|
||||
visible: root.showBackground
|
||||
|
||||
layer.enabled: Settings.data.general.enableShadows && !internal.isDragging && root.showBackground
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowBlur: Style.shadowBlur * 1.5
|
||||
shadowOpacity: Style.shadowOpacity * 0.6
|
||||
shadowColor: "black"
|
||||
shadowHorizontalOffset: Settings.data.general.shadowOffsetX
|
||||
shadowVerticalOffset: Settings.data.general.shadowOffsetY
|
||||
blurMax: Style.shadowBlurMax
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.fill: parent
|
||||
z: 1
|
||||
clip: true
|
||||
}
|
||||
|
||||
// Context menu model and handler - menu is created dynamically in PopupMenuWindow
|
||||
property var contextMenuModel: {
|
||||
var hasSettings = false;
|
||||
if (widgetData && widgetData.id) {
|
||||
var widgetId = widgetData.id;
|
||||
if (DesktopWidgetRegistry.isPluginWidget(widgetId)) {
|
||||
var pluginId = widgetId.replace("plugin:", "");
|
||||
var manifest = PluginRegistry.getPluginManifest(pluginId);
|
||||
hasSettings = manifest && manifest.entryPoints && (manifest.entryPoints.settings || manifest.entryPoints.desktopWidgetSettings);
|
||||
} else {
|
||||
hasSettings = DesktopWidgetRegistry.widgetSettingsMap[widgetId] !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
var items = [];
|
||||
if (hasSettings) {
|
||||
items.push({
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
"label": I18n.tr("common.reset"),
|
||||
"action": "reset",
|
||||
"icon": "restore"
|
||||
});
|
||||
items.push({
|
||||
"label": I18n.tr("actions.raise-to-top"),
|
||||
"action": "raise-to-top",
|
||||
"icon": "stack-front"
|
||||
});
|
||||
items.push({
|
||||
"label": I18n.tr("actions.lower-to-bottom"),
|
||||
"action": "lower-to-bottom",
|
||||
"icon": "stack-back"
|
||||
});
|
||||
items.push({
|
||||
"label": I18n.tr("common.delete"),
|
||||
"action": "delete",
|
||||
"icon": "trash"
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
// Drag MouseArea - handles dragging (left-click)
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
visible: DesktopWidgetRegistry.editMode
|
||||
cursorShape: internal.isDragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
property point pressPos: Qt.point(0, 0)
|
||||
|
||||
onPressed: mouse => {
|
||||
// Prevent starting new operation if one is already in progress
|
||||
if (internal.operationType !== "") {
|
||||
return;
|
||||
}
|
||||
|
||||
pressPos = Qt.point(mouse.x, mouse.y);
|
||||
internal.operationType = "drag";
|
||||
internal.dragOffsetX = root.x;
|
||||
internal.dragOffsetY = root.y;
|
||||
internal.isDragging = true;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (internal.isDragging && pressed && internal.operationType === "drag") {
|
||||
var globalPressPos = mapToItem(root.parent, pressPos.x, pressPos.y);
|
||||
var globalCurrentPos = mapToItem(root.parent, mouse.x, mouse.y);
|
||||
|
||||
var deltaX = globalCurrentPos.x - globalPressPos.x;
|
||||
var deltaY = globalCurrentPos.y - globalPressPos.y;
|
||||
|
||||
var newX = internal.dragOffsetX + deltaX;
|
||||
var newY = internal.dragOffsetY + deltaY;
|
||||
|
||||
// Boundary clamping - allow widgets to go partially off-screen (75% can clip)
|
||||
// This gives users more control over positioning while keeping widgets accessible
|
||||
var scaledWidth = root.width;
|
||||
var scaledHeight = root.height;
|
||||
if (root.parent && scaledWidth > 0 && scaledHeight > 0) {
|
||||
var minVisibleX = scaledWidth * 0.25;
|
||||
var minVisibleY = scaledHeight * 0.25;
|
||||
newX = Math.max(-scaledWidth + minVisibleX, Math.min(newX, root.parent.width - minVisibleX));
|
||||
newY = Math.max(-scaledHeight + minVisibleY, Math.min(newY, root.parent.height - minVisibleY));
|
||||
}
|
||||
|
||||
if (Settings.data.desktopWidgets.gridSnap) {
|
||||
newX = root.snapToGrid(newX);
|
||||
newY = root.snapToGrid(newY);
|
||||
// Re-clamp after snapping
|
||||
if (root.parent && scaledWidth > 0 && scaledHeight > 0) {
|
||||
var minVisibleX = scaledWidth * 0.25;
|
||||
var minVisibleY = scaledHeight * 0.25;
|
||||
newX = Math.max(-scaledWidth + minVisibleX, Math.min(newX, root.parent.width - minVisibleX));
|
||||
newY = Math.max(-scaledHeight + minVisibleY, Math.min(newY, root.parent.height - minVisibleY));
|
||||
}
|
||||
}
|
||||
|
||||
internal.dragOffsetX = newX;
|
||||
internal.dragOffsetY = newY;
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
if (internal.isDragging && internal.operationType === "drag" && widgetIndex >= 0 && screen && screen.name) {
|
||||
var roundedX = Math.round(internal.dragOffsetX);
|
||||
var roundedY = Math.round(internal.dragOffsetY);
|
||||
root.updateWidgetData({
|
||||
"x": roundedX,
|
||||
"y": roundedY
|
||||
});
|
||||
|
||||
internal.baseX = roundedX;
|
||||
internal.baseY = roundedY;
|
||||
internal.isDragging = false;
|
||||
internal.operationType = "";
|
||||
}
|
||||
}
|
||||
|
||||
onCanceled: {
|
||||
internal.isDragging = false;
|
||||
internal.operationType = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Right-click MouseArea for context menu
|
||||
MouseArea {
|
||||
id: contextMenuArea
|
||||
anchors.fill: parent
|
||||
z: 1001
|
||||
visible: DesktopWidgetRegistry.editMode
|
||||
acceptedButtons: Qt.RightButton
|
||||
hoverEnabled: true
|
||||
|
||||
onPressed: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
var popupMenuWindow = PanelService.getPopupMenuWindow(root.screen);
|
||||
if (popupMenuWindow) {
|
||||
// Map click position to screen coordinates
|
||||
var globalPos = root.mapToItem(null, mouse.x, mouse.y);
|
||||
// Use dynamic context menu (created in PopupMenuWindow's Top layer)
|
||||
// This ensures input events work correctly for desktop widgets (Bottom layer)
|
||||
popupMenuWindow.showDynamicContextMenu(root.contextMenuModel, globalPos.x, globalPos.y, root.handleContextMenuAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Corner handles for scaling - using Repeater to avoid code duplication
|
||||
readonly property real cornerHandleSize: 8 * widgetScale
|
||||
readonly property real outlineMargin: Style.marginS * widgetScale
|
||||
readonly property color colorHandle: Color.mSecondary
|
||||
|
||||
// Corner handle model: defines position, direction, cursor, and triangle points for each corner
|
||||
// xMult/yMult: multipliers for position (0 = left/top edge, 1 = right/bottom edge)
|
||||
// xDir/yDir: direction multiplier for scaling (-1 = left/up increases, 1 = right/down increases)
|
||||
// cursor: resize cursor type (FDiag for TL-BR diagonal, BDiag for TR-BL diagonal)
|
||||
// points: triangle vertices as [x, y] pairs normalized to cornerHandleSize
|
||||
readonly property var cornerHandleModel: [
|
||||
{
|
||||
xMult: 0,
|
||||
yMult: 0,
|
||||
xDir: -1,
|
||||
yDir: -1,
|
||||
cursor: Qt.SizeFDiagCursor,
|
||||
points: [[0, 0], [1, 0], [0, 1]]
|
||||
},
|
||||
{
|
||||
xMult: 1,
|
||||
yMult: 0,
|
||||
xDir: 1,
|
||||
yDir: -1,
|
||||
cursor: Qt.SizeBDiagCursor,
|
||||
points: [[1, 0], [1, 1], [0, 0]]
|
||||
},
|
||||
{
|
||||
xMult: 0,
|
||||
yMult: 1,
|
||||
xDir: -1,
|
||||
yDir: 1,
|
||||
cursor: Qt.SizeBDiagCursor,
|
||||
points: [[0, 1], [0, 0], [1, 1]]
|
||||
},
|
||||
{
|
||||
xMult: 1,
|
||||
yMult: 1,
|
||||
xDir: 1,
|
||||
yDir: 1,
|
||||
cursor: Qt.SizeFDiagCursor,
|
||||
points: [[1, 1], [1, 0], [0, 1]]
|
||||
}
|
||||
]
|
||||
|
||||
Repeater {
|
||||
model: root.cornerHandleModel
|
||||
|
||||
delegate: Canvas {
|
||||
id: cornerHandle
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
visible: DesktopWidgetRegistry.editMode && !internal.isDragging
|
||||
// Position handles at corners of decoration rectangle (which extends by outlineMargin)
|
||||
x: modelData.xMult === 0 ? -outlineMargin : (root.width + outlineMargin - cornerHandleSize)
|
||||
y: modelData.yMult === 0 ? -outlineMargin : (root.height + outlineMargin - cornerHandleSize)
|
||||
width: cornerHandleSize
|
||||
height: cornerHandleSize
|
||||
z: 2000
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
ctx.fillStyle = colorHandle;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(modelData.points[0][0] * cornerHandleSize, modelData.points[0][1] * cornerHandleSize);
|
||||
ctx.lineTo(modelData.points[1][0] * cornerHandleSize, modelData.points[1][1] * cornerHandleSize);
|
||||
ctx.lineTo(modelData.points[2][0] * cornerHandleSize, modelData.points[2][1] * cornerHandleSize);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
Component.onCompleted: requestPaint()
|
||||
onVisibleChanged: if (visible)
|
||||
requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onWidthChanged() {
|
||||
if (cornerHandle.visible)
|
||||
cornerHandle.requestPaint();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
if (cornerHandle.visible)
|
||||
cornerHandle.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: scaleMouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: cornerHandle.modelData.cursor
|
||||
property point pressPos: Qt.point(0, 0)
|
||||
|
||||
onPressed: mouse => {
|
||||
if (internal.operationType !== "") {
|
||||
return;
|
||||
}
|
||||
pressPos = mapToItem(root.parent, mouse.x, mouse.y);
|
||||
internal.operationType = "scale";
|
||||
internal.isScaling = true;
|
||||
internal.initialScale = root.widgetScale;
|
||||
internal.lastScale = root.widgetScale;
|
||||
internal.initialWidth = root.width;
|
||||
internal.initialHeight = root.height;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (internal.isScaling && pressed && internal.operationType === "scale") {
|
||||
var currentPos = mapToItem(root.parent, mouse.x, mouse.y);
|
||||
var deltaX = currentPos.x - pressPos.x;
|
||||
var deltaY = currentPos.y - pressPos.y;
|
||||
|
||||
// Project delta onto the diagonal direction for this corner
|
||||
// xDir/yDir indicate which direction increases scale
|
||||
var diagonalDelta = (deltaX * cornerHandle.modelData.xDir + deltaY * cornerHandle.modelData.yDir) / Math.sqrt(2);
|
||||
|
||||
// Scale sensitivity: pixels of drag per 1.0 scale change
|
||||
var sensitivity = 150;
|
||||
var scaleDelta = diagonalDelta / sensitivity;
|
||||
var newScale = Math.max(root.minScale, Math.min(root.maxScale, internal.initialScale + scaleDelta));
|
||||
|
||||
newScale = root.snapScaleToGrid(newScale);
|
||||
|
||||
if (!isNaN(newScale) && newScale > 0) {
|
||||
root.widgetScale = newScale;
|
||||
internal.lastScale = newScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
if (internal.isScaling && internal.operationType === "scale") {
|
||||
root.updateWidgetData({
|
||||
"scale": root.widgetScale
|
||||
});
|
||||
internal.isScaling = false;
|
||||
internal.operationType = "";
|
||||
root.widgetScale = root.snapScaleToGrid(root.widgetScale);
|
||||
internal.lastScale = root.widgetScale;
|
||||
}
|
||||
}
|
||||
|
||||
onCanceled: {
|
||||
internal.isScaling = false;
|
||||
internal.operationType = "";
|
||||
internal.lastScale = root.widgetScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import qs.Widgets.AudioSpectrum
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
|
||||
defaultY: 280
|
||||
|
||||
readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["AudioVisualizer"]
|
||||
|
||||
readonly property int visualizerWidth: (widgetData && widgetData.width !== undefined) ? widgetData.width : (widgetMetadata?.width ?? 320)
|
||||
readonly property int visualizerHeight: (widgetData && widgetData.height !== undefined) ? widgetData.height : (widgetMetadata?.height ?? 72)
|
||||
readonly property string visualizerType: (widgetData && widgetData.visualizerType !== undefined) ? widgetData.visualizerType : (widgetMetadata?.visualizerType ?? "linear")
|
||||
readonly property bool hideWhenIdle: (widgetData && widgetData.hideWhenIdle !== undefined) ? widgetData.hideWhenIdle : (widgetMetadata?.hideWhenIdle ?? false)
|
||||
readonly property string colorName: (widgetData && widgetData.colorName !== undefined) ? widgetData.colorName : (widgetMetadata?.colorName ?? "primary")
|
||||
|
||||
readonly property color fillColor: Color.resolveColorKey(colorName)
|
||||
|
||||
readonly property bool shouldShow: visualizerType !== "" && visualizerType !== "none" && (!hideWhenIdle || MediaService.isPlaying)
|
||||
readonly property bool isHidden: !shouldShow
|
||||
readonly property bool shouldRegisterSpectrum: shouldShow
|
||||
|
||||
// Keep widget visible in edit mode so users can move/configure it
|
||||
visible: !root.isHidden || DesktopWidgetRegistry.editMode
|
||||
|
||||
readonly property string spectrumComponentId: "desktop:audiovisualizer:" + (root.screen ? root.screen.name : "unknown") + ":" + root.widgetIndex
|
||||
|
||||
onShouldRegisterSpectrumChanged: {
|
||||
if (root.shouldRegisterSpectrum) {
|
||||
SpectrumService.registerComponent(root.spectrumComponentId);
|
||||
} else {
|
||||
SpectrumService.unregisterComponent(root.spectrumComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.shouldRegisterSpectrum) {
|
||||
SpectrumService.registerComponent(root.spectrumComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SpectrumService.unregisterComponent(root.spectrumComponentId);
|
||||
}
|
||||
|
||||
implicitWidth: Math.round(visualizerWidth * widgetScale)
|
||||
implicitHeight: Math.round(visualizerHeight * widgetScale)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
Rectangle {
|
||||
id: visualizerMask
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
radius: root.roundedCorners ? Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2) : 0
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
id: visualizerLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.showBackground ? Math.round(Style.marginXS * root.widgetScale) : 0
|
||||
active: root.shouldShow
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: {
|
||||
switch (root.visualizerType) {
|
||||
case "linear":
|
||||
return linearComponent;
|
||||
case "mirrored":
|
||||
return mirroredComponent;
|
||||
case "wave":
|
||||
return waveComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: linearComponent
|
||||
NLinearSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: root.fillColor
|
||||
showMinimumSignal: true
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mirroredComponent
|
||||
NMirroredSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: root.fillColor
|
||||
showMinimumSignal: true
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: waveComponent
|
||||
NWaveSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: root.fillColor
|
||||
showMinimumSignal: true
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
|
||||
readonly property var now: Time.now
|
||||
readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["Clock"]
|
||||
|
||||
readonly property color clockTextColor: Color.resolveColorKey(clockColor)
|
||||
readonly property real fontSize: Math.round(Style.fontSizeXXXL * 2.5 * widgetScale)
|
||||
readonly property real widgetOpacity: widgetData.opacity !== undefined ? widgetData.opacity : 1.0
|
||||
readonly property string clockStyle: widgetData.clockStyle !== undefined ? widgetData.clockStyle : widgetMetadata.clockStyle
|
||||
readonly property string clockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor
|
||||
readonly property bool useCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
|
||||
readonly property string customFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont
|
||||
readonly property string format: widgetData.format !== undefined ? widgetData.format : widgetMetadata.format
|
||||
|
||||
readonly property real contentPadding: Math.round((clockStyle === "minimal" ? Style.marginL : Style.marginXL) * widgetScale)
|
||||
implicitWidth: contentLoader.item ? Math.round((contentLoader.item.implicitWidth || contentLoader.item.width || 0) + contentPadding * 2) : 0
|
||||
implicitHeight: contentLoader.item ? Math.round((contentLoader.item.implicitHeight || contentLoader.item.height || 0) + contentPadding * 2) : 0
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
Component {
|
||||
id: nclockComponent
|
||||
NClock {
|
||||
now: root.now
|
||||
clockStyle: root.clockStyle
|
||||
backgroundColor: "transparent"
|
||||
clockColor: clockTextColor
|
||||
progressColor: Color.mPrimary
|
||||
opacity: root.widgetOpacity
|
||||
height: Math.round(fontSize * 1.9)
|
||||
width: height
|
||||
hoursFontSize: fontSize * 0.6
|
||||
minutesFontSize: fontSize * 0.4
|
||||
scaleRatio: root.widgetScale
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: minimalClockComponent
|
||||
ColumnLayout {
|
||||
spacing: -2
|
||||
opacity: root.widgetOpacity
|
||||
|
||||
Repeater {
|
||||
model: I18n.locale.toString(root.now, root.format.trim()).split("\\n")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: root.useCustomFont && root.customFont ? root.customFont : Settings.data.ui.fontDefault
|
||||
pointSize: {
|
||||
if (model.length == 1) {
|
||||
return Math.round(Style.fontSizeXXL * root.widgetScale);
|
||||
} else {
|
||||
return Math.round((index == 0) ? Style.fontSizeXXL * root.widgetScale : Style.fontSizeM * root.widgetScale);
|
||||
}
|
||||
}
|
||||
font.weight: Style.fontWeightBold
|
||||
color: root.clockTextColor
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.centerIn: parent
|
||||
z: 2
|
||||
sourceComponent: clockStyle === "minimal" ? minimalClockComponent : nclockComponent
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import qs.Widgets.AudioSpectrum
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
|
||||
defaultY: 200
|
||||
|
||||
// Widget settings - check widgetData exists before accessing properties
|
||||
readonly property string hideMode: (widgetData && widgetData.hideMode !== undefined) ? widgetData.hideMode : "visible"
|
||||
readonly property bool showButtons: (widgetData && widgetData.showButtons !== undefined) ? widgetData.showButtons : true
|
||||
readonly property bool showAlbumArt: (widgetData && widgetData.showAlbumArt !== undefined) ? widgetData.showAlbumArt : true
|
||||
readonly property bool showVisualizer: (widgetData && widgetData.showVisualizer !== undefined) ? widgetData.showVisualizer : true
|
||||
readonly property string visualizerType: (widgetData && widgetData.visualizerType && widgetData.visualizerType !== "") ? widgetData.visualizerType : "linear"
|
||||
readonly property bool roundedCorners: (widgetData && widgetData.roundedCorners !== undefined) ? widgetData.roundedCorners : true
|
||||
readonly property bool hasPlayer: MediaService.currentPlayer !== null
|
||||
readonly property bool isPlaying: MediaService.isPlaying
|
||||
readonly property bool hasActiveTrack: hasPlayer && (MediaService.trackTitle || MediaService.trackArtist)
|
||||
|
||||
// State
|
||||
// Hide when idle when playback is not active
|
||||
readonly property bool shouldHideIdle: (hideMode === "idle") && !isPlaying
|
||||
readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden"
|
||||
readonly property bool isHidden: (shouldHideIdle || shouldHideEmpty) && !DesktopWidgetRegistry.editMode
|
||||
visible: !isHidden
|
||||
|
||||
// SpectrumService registration for visualizer
|
||||
readonly property string spectrumComponentId: "desktopmediaplayer:" + (root.screen ? root.screen.name : "unknown")
|
||||
readonly property bool needsSpectrum: root.shouldShowVisualizer && !root.isHidden
|
||||
|
||||
onNeedsSpectrumChanged: {
|
||||
if (root.needsSpectrum) {
|
||||
SpectrumService.registerComponent(root.spectrumComponentId);
|
||||
} else {
|
||||
SpectrumService.unregisterComponent(root.spectrumComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.needsSpectrum) {
|
||||
SpectrumService.registerComponent(root.spectrumComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SpectrumService.unregisterComponent(root.spectrumComponentId);
|
||||
}
|
||||
|
||||
readonly property bool showPrev: hasPlayer && MediaService.canGoPrevious
|
||||
readonly property bool showNext: hasPlayer && MediaService.canGoNext
|
||||
readonly property int visibleButtonCount: root.showButtons ? (1 + (showPrev ? 1 : 0) + (showNext ? 1 : 0)) : 0
|
||||
|
||||
implicitWidth: Math.round(400 * widgetScale)
|
||||
implicitHeight: Math.round(64 * widgetScale + Style.margin2M * widgetScale)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
// Visualizer visibility mode
|
||||
readonly property bool shouldShowVisualizer: {
|
||||
if (!root.showVisualizer)
|
||||
return false;
|
||||
if (root.visualizerType === "" || root.visualizerType === "none")
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Visualizer overlay (visibility controlled by visualizerVisibility setting)
|
||||
// Completely disabled during scaling to avoid expensive canvas redraws
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Math.round(Style.marginXS * widgetScale)
|
||||
anchors.rightMargin: Math.round(Style.marginXS * widgetScale)
|
||||
anchors.topMargin: Math.round(Style.marginXS * widgetScale)
|
||||
anchors.bottomMargin: 0
|
||||
z: 0
|
||||
clip: true
|
||||
active: needsSpectrum
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: root.roundedCorners
|
||||
maskSource: ShaderEffectSource {
|
||||
sourceItem: Rectangle {
|
||||
width: root.width - Math.round(Style.marginXS * widgetScale) * 2
|
||||
height: root.height - Math.round(Style.marginXS * widgetScale)
|
||||
radius: root.roundedCorners ? Math.round(Math.max(0, (Style.radiusL - Style.marginXS) * widgetScale)) : 0
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: {
|
||||
switch (root.visualizerType) {
|
||||
case "linear":
|
||||
return linearComponent;
|
||||
case "mirrored":
|
||||
return mirroredComponent;
|
||||
case "wave":
|
||||
return waveComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: linearComponent
|
||||
NLinearSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.5
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mirroredComponent
|
||||
NMirroredSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.5
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: waveComponent
|
||||
NWaveSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.5
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drop shadow for text and controls readability over visualizer
|
||||
// Disabled during scaling to avoid expensive recomputations
|
||||
NDropShadow {
|
||||
visible: !root.isScaling
|
||||
anchors.fill: contentLayout
|
||||
source: contentLayout
|
||||
z: 1
|
||||
autoPaddingEnabled: true
|
||||
shadowBlur: 1.0
|
||||
shadowOpacity: 0.9
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
states: [
|
||||
State {
|
||||
when: root.showButtons
|
||||
AnchorChanges {
|
||||
target: contentLayout
|
||||
anchors.horizontalCenter: undefined
|
||||
anchors.verticalCenter: undefined
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
},
|
||||
State {
|
||||
when: !root.showButtons
|
||||
AnchorChanges {
|
||||
target: contentLayout
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.top: undefined
|
||||
anchors.bottom: undefined
|
||||
anchors.left: undefined
|
||||
anchors.right: undefined
|
||||
}
|
||||
}
|
||||
]
|
||||
anchors.margins: Math.round(Style.marginM * widgetScale)
|
||||
spacing: Math.round(Style.marginS * widgetScale)
|
||||
z: 2
|
||||
|
||||
Item {
|
||||
visible: root.showAlbumArt
|
||||
Layout.preferredWidth: Math.round(48 * widgetScale)
|
||||
Layout.preferredHeight: Math.round(48 * widgetScale)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NImageRounded {
|
||||
visible: hasPlayer
|
||||
anchors.fill: parent
|
||||
radius: Math.round(Style.radiusM * widgetScale)
|
||||
imagePath: MediaService.trackArtUrl
|
||||
imageFillMode: Image.PreserveAspectCrop
|
||||
fallbackIcon: isPlaying ? "media-pause" : "media-play"
|
||||
fallbackIconSize: Math.round(20 * widgetScale)
|
||||
borderWidth: 0
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: !hasPlayer
|
||||
anchors.centerIn: parent
|
||||
icon: "disc"
|
||||
pointSize: Math.round(24 * widgetScale)
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
visible: root.showAlbumArt
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: root.showButtons ? Qt.AlignVCenter : Qt.AlignCenter
|
||||
spacing: 0
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: hasPlayer ? (MediaService.trackTitle || "Unknown Track") : "No media playing"
|
||||
pointSize: Math.round(Style.fontSizeS * widgetScale)
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: hasPlayer && MediaService.trackArtist
|
||||
Layout.fillWidth: true
|
||||
text: MediaService.trackArtist || ""
|
||||
pointSize: Math.round(Style.fontSizeXS * widgetScale)
|
||||
font.weight: Style.fontWeightRegular
|
||||
color: Color.mSecondary
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: controlsRow
|
||||
spacing: Math.round(Style.marginXS * widgetScale)
|
||||
z: 10
|
||||
visible: root.showButtons
|
||||
Layout.alignment: root.showAlbumArt ? Qt.AlignVCenter : Qt.AlignCenter
|
||||
|
||||
NIconButton {
|
||||
opacity: showPrev ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
baseSize: Math.round(32 * widgetScale)
|
||||
icon: "media-prev"
|
||||
enabled: hasPlayer && MediaService.canGoPrevious
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
customRadius: Math.round(Style.radiusS * widgetScale)
|
||||
onClicked: {
|
||||
if (enabled)
|
||||
MediaService.previous();
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
baseSize: Math.round(36 * widgetScale)
|
||||
icon: isPlaying ? "media-pause" : "media-play"
|
||||
enabled: hasPlayer && (MediaService.canPlay || MediaService.canPause)
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
colorBgHover: Qt.lighter(Color.mPrimary, 1.1)
|
||||
colorFgHover: Color.mOnPrimary
|
||||
customRadius: Math.round(Style.radiusS * widgetScale)
|
||||
onClicked: {
|
||||
if (enabled) {
|
||||
MediaService.playPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
opacity: showNext ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationSlow
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
baseSize: Math.round(32 * widgetScale)
|
||||
icon: "media-next"
|
||||
enabled: hasPlayer && MediaService.canGoNext
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
customRadius: Math.round(Style.radiusS * widgetScale)
|
||||
onClicked: {
|
||||
if (enabled)
|
||||
MediaService.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
|
||||
// Widget settings
|
||||
readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["SystemStat"]
|
||||
readonly property string statType: (widgetData && widgetData.statType !== undefined) ? widgetData.statType : (widgetMetadata.statType !== undefined ? widgetMetadata.statType : "CPU")
|
||||
readonly property string diskPath: (widgetData && widgetData.diskPath !== undefined) ? widgetData.diskPath : "/"
|
||||
readonly property string layout: (widgetData && widgetData.layout !== undefined) ? widgetData.layout : (widgetMetadata.layout !== undefined ? widgetMetadata.layout : "side")
|
||||
|
||||
// Fixed colors
|
||||
readonly property color color: Color.mPrimary
|
||||
readonly property color color2: Color.mSecondary
|
||||
|
||||
// Legend items model - each item has: text, color, icon (optional), bold (optional), opacity (optional), elide (optional)
|
||||
readonly property var legendItems: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
return [
|
||||
{
|
||||
icon: "cpu-usage",
|
||||
text: Math.round(SystemStatService.cpuUsage) + "%",
|
||||
color: root.color
|
||||
},
|
||||
{
|
||||
text: "cpu-usage",
|
||||
text: SystemStatService.cpuFreq,
|
||||
color: root.color,
|
||||
opacity: 0.8
|
||||
},
|
||||
{
|
||||
icon: "cpu-temperature",
|
||||
text: SystemStatService.cpuTemp + "°C",
|
||||
color: root.color2
|
||||
}
|
||||
];
|
||||
case "GPU":
|
||||
return [
|
||||
{
|
||||
icon: "gpu-temperature",
|
||||
text: Math.round(SystemStatService.gpuTemp) + "°C",
|
||||
color: root.color
|
||||
}
|
||||
];
|
||||
case "Memory":
|
||||
return [
|
||||
{
|
||||
icon: "memory",
|
||||
text: Math.round(SystemStatService.memPercent) + "%",
|
||||
color: root.color
|
||||
}
|
||||
];
|
||||
case "Disk":
|
||||
var items = [
|
||||
{
|
||||
icon: "storage",
|
||||
text: Math.round(SystemStatService.diskPercents[root.diskPath] || 0) + "%",
|
||||
color: root.color
|
||||
}
|
||||
];
|
||||
if (root.diskPath !== "/") {
|
||||
items.push({
|
||||
text: root.diskPath,
|
||||
color: root.color,
|
||||
opacity: 0.8,
|
||||
elide: true
|
||||
});
|
||||
}
|
||||
return items;
|
||||
case "Network":
|
||||
return [
|
||||
{
|
||||
icon: "download-speed",
|
||||
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed),
|
||||
color: root.color
|
||||
},
|
||||
{
|
||||
icon: "upload-speed",
|
||||
text: SystemStatService.formatSpeed(SystemStatService.txSpeed),
|
||||
color: root.color2
|
||||
}
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// History from service
|
||||
readonly property var history: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
return SystemStatService.cpuHistory;
|
||||
case "GPU":
|
||||
return SystemStatService.gpuTempHistory;
|
||||
case "Memory":
|
||||
return SystemStatService.memHistory;
|
||||
case "Disk":
|
||||
return SystemStatService.diskHistories[root.diskPath] || [];
|
||||
case "Network":
|
||||
return SystemStatService.rxSpeedHistory;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary history (CPU temp for CPU, Tx for Network)
|
||||
readonly property var history2: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
return SystemStatService.cpuTempHistory;
|
||||
case "Network":
|
||||
return SystemStatService.txSpeedHistory;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Graph min/max values
|
||||
readonly property real graphMinValue: root.statType === "GPU" ? Math.max(SystemStatService.gpuTempHistoryMin - 5, 0) : 0
|
||||
readonly property real graphMaxValue: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
case "Memory":
|
||||
case "Disk":
|
||||
return 100; // Percentage-based stats use fixed 0-100 range
|
||||
case "GPU":
|
||||
return Math.max(SystemStatService.gpuTempHistoryMax + 5, 1);
|
||||
case "Network":
|
||||
return SystemStatService.rxMaxSpeed;
|
||||
default:
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
readonly property real graphMinValue2: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
return Math.max(SystemStatService.cpuTempHistoryMin - 5, 0);
|
||||
default:
|
||||
return graphMinValue;
|
||||
}
|
||||
}
|
||||
readonly property real graphMaxValue2: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
return Math.max(SystemStatService.cpuTempHistoryMax + 5, 1);
|
||||
case "Network":
|
||||
return SystemStatService.txMaxSpeed;
|
||||
default:
|
||||
return graphMaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: SystemStatService.registerComponent("desktop-sysstat:" + root.statType)
|
||||
Component.onDestruction: SystemStatService.unregisterComponent("desktop-sysstat:" + root.statType)
|
||||
|
||||
implicitWidth: Math.round(240 * widgetScale)
|
||||
implicitHeight: Math.round(120 * widgetScale)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
// Update interval per stat type
|
||||
readonly property int graphUpdateInterval: {
|
||||
switch (root.statType) {
|
||||
case "CPU":
|
||||
return SystemStatService.cpuUsageIntervalMs;
|
||||
case "GPU":
|
||||
return SystemStatService.gpuIntervalMs;
|
||||
case "Memory":
|
||||
return SystemStatService.memIntervalMs;
|
||||
case "Disk":
|
||||
return SystemStatService.diskIntervalMs;
|
||||
case "Network":
|
||||
return SystemStatService.networkIntervalMs;
|
||||
default:
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// Graph component (shared between layouts)
|
||||
Component {
|
||||
id: graphComponent
|
||||
NGraph {
|
||||
values: root.history
|
||||
values2: root.history2
|
||||
minValue: root.graphMinValue
|
||||
maxValue: root.graphMaxValue
|
||||
minValue2: root.graphMinValue2
|
||||
maxValue2: root.graphMaxValue2
|
||||
color: root.color
|
||||
color2: root.color2
|
||||
fill: true
|
||||
updateInterval: root.graphUpdateInterval
|
||||
strokeWidth: Math.max(1, root.widgetScale)
|
||||
animateScale: root.statType === "Network"
|
||||
}
|
||||
}
|
||||
|
||||
// Side layout: icon + legend on left, graph on right
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Math.round(Style.marginM * widgetScale)
|
||||
spacing: Math.round(Style.marginL * widgetScale)
|
||||
visible: root.layout === "side"
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: Math.round(64 * widgetScale)
|
||||
spacing: Style.marginXS * root.widgetScale
|
||||
|
||||
Repeater {
|
||||
model: root.legendItems
|
||||
delegate: RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Math.round(Style.marginXXS * root.widgetScale)
|
||||
|
||||
NIcon {
|
||||
visible: !!modelData.icon
|
||||
icon: modelData.icon || ""
|
||||
color: modelData.color
|
||||
pointSize: Style.fontSizeS * root.widgetScale
|
||||
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.text
|
||||
color: modelData.color
|
||||
pointSize: Style.fontSizeS * root.widgetScale
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: modelData.bold ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
|
||||
elide: modelData.elide ? Text.ElideMiddle : Text.ElideNone
|
||||
Layout.maximumWidth: modelData.elide ? Math.round(56 * root.widgetScale) : -1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.layout === "side"
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
sourceComponent: graphComponent
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom layout: full-width graph, horizontal legend at bottom
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Math.round(Style.marginM * widgetScale)
|
||||
spacing: Math.round(Style.marginS * widgetScale)
|
||||
visible: root.layout === "bottom"
|
||||
|
||||
Loader {
|
||||
active: root.layout === "bottom"
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
sourceComponent: graphComponent
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Math.round(Style.marginM * widgetScale)
|
||||
|
||||
Repeater {
|
||||
model: root.legendItems
|
||||
delegate: RowLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Math.round(Style.marginXXS * root.widgetScale)
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !!modelData.icon
|
||||
icon: modelData.icon || ""
|
||||
color: modelData.color
|
||||
pointSize: Style.fontSizeS * root.widgetScale
|
||||
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: modelData.text
|
||||
color: modelData.color
|
||||
pointSize: Style.fontSizeS * root.widgetScale
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.weight: modelData.bold ? Style.fontWeightBold : Style.fontWeightRegular
|
||||
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
|
||||
elide: modelData.elide ? Text.ElideMiddle : Text.ElideNone
|
||||
Layout.maximumWidth: modelData.elide ? Math.round(56 * root.widgetScale) : -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Services.Location
|
||||
import qs.Widgets
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
|
||||
readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
|
||||
readonly property int currentWeatherCode: weatherReady ? LocationService.data.weather.current_weather.weathercode : 0
|
||||
readonly property real currentTemp: {
|
||||
if (!weatherReady)
|
||||
return 0;
|
||||
var temp = LocationService.data.weather.current_weather.temperature;
|
||||
if (Settings.data.location.useFahrenheit) {
|
||||
temp = LocationService.celsiusToFahrenheit(temp);
|
||||
}
|
||||
return Math.round(temp);
|
||||
}
|
||||
readonly property real todayMax: {
|
||||
if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_max.length === 0)
|
||||
return 0;
|
||||
var temp = LocationService.data.weather.daily.temperature_2m_max[0];
|
||||
if (Settings.data.location.useFahrenheit) {
|
||||
temp = LocationService.celsiusToFahrenheit(temp);
|
||||
}
|
||||
return Math.round(temp);
|
||||
}
|
||||
readonly property real todayMin: {
|
||||
if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_min.length === 0)
|
||||
return 0;
|
||||
var temp = LocationService.data.weather.daily.temperature_2m_min[0];
|
||||
if (Settings.data.location.useFahrenheit) {
|
||||
temp = LocationService.celsiusToFahrenheit(temp);
|
||||
}
|
||||
return Math.round(temp);
|
||||
}
|
||||
readonly property string tempUnit: Settings.data.location.useFahrenheit ? "F" : "C"
|
||||
readonly property string locationName: {
|
||||
const chunks = Settings.data.location.name.split(",");
|
||||
return chunks[0];
|
||||
}
|
||||
|
||||
implicitWidth: Math.round(Math.max(240 * widgetScale, contentLayout.implicitWidth + Style.margin2M * widgetScale))
|
||||
implicitHeight: Math.round(64 * widgetScale + Style.margin2M * widgetScale)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Math.round(Style.marginM * widgetScale)
|
||||
spacing: Math.round(Style.marginM * widgetScale)
|
||||
z: 2
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: Math.round(64 * widgetScale)
|
||||
Layout.preferredHeight: Math.round(64 * widgetScale)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NIcon {
|
||||
visible: !LocationService.taliaWeatherMascotActive || !weatherReady
|
||||
anchors.centerIn: parent
|
||||
icon: weatherReady ? LocationService.weatherSymbolFromCode(currentWeatherCode) : (LocationService.locationConfigured ? "weather-cloud-off" : "map-pin-off")
|
||||
pointSize: Math.round(Style.fontSizeXXXL * 2 * widgetScale)
|
||||
color: weatherReady ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
Loader {
|
||||
active: LocationService.taliaWeatherMascotActive && weatherReady
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
sourceComponent: Component {
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(currentWeatherCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: weatherReady ? `${currentTemp}°${tempUnit}` : "--"
|
||||
pointSize: Math.round(Style.fontSizeXXXL * widgetScale)
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Math.round(Style.marginXXS * widgetScale)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: locationName || I18n.tr("common.weather-no-location")
|
||||
pointSize: Math.round(Style.fontSizeS * widgetScale)
|
||||
font.weight: Style.fontWeightRegular
|
||||
color: Color.mOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: !Settings.data.location.hideWeatherCityName
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Math.round(Style.marginXS * widgetScale)
|
||||
visible: weatherReady && todayMax > 0 && todayMin > 0
|
||||
|
||||
NText {
|
||||
text: "H:"
|
||||
pointSize: Math.round(Style.fontSizeXS * widgetScale)
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
NText {
|
||||
text: `${todayMax}°`
|
||||
pointSize: Math.round(Style.fontSizeXS * widgetScale)
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "•"
|
||||
pointSize: Math.round(Style.fontSizeXXS * widgetScale)
|
||||
color: Color.mOnSurfaceVariant
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "L:"
|
||||
pointSize: Math.round(Style.fontSizeXS * widgetScale)
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
NText {
|
||||
text: `${todayMin}°`
|
||||
pointSize: Math.round(Style.fontSizeXS * widgetScale)
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user