Merge branch 'noctalia-dev:main' into pr/networking-refactor-pt1

This commit is contained in:
Turann_
2026-03-10 04:26:21 +03:00
committed by GitHub
10 changed files with 152 additions and 42 deletions
+1
View File
@@ -1021,6 +1021,7 @@
"edit-mode-description": "Enable edit mode to move and reposition desktop widgets. When enabled, widgets show a drag outline and can be repositioned.",
"edit-mode-exit-button": "Exit edit mode",
"edit-mode-grid-snap-label": "Grid snap",
"edit-mode-grid-snap-scale-label": "Snap scale",
"edit-mode-label": "Edit mode",
"enabled-description": "Enable or disable desktop widgets entirely.",
"enabled-label": "Enable desktop widgets",
+1
View File
@@ -539,6 +539,7 @@
"enabled": false,
"overviewEnabled": true,
"gridSnap": false,
"gridSnapScale": false,
"monitorWidgets": []
}
}
+1
View File
@@ -763,6 +763,7 @@ Singleton {
property bool enabled: false
property bool overviewEnabled: true
property bool gridSnap: false
property bool gridSnapScale: false
property list<var> monitorWidgets: []
// Format: [{ "name": "DP-1", "widgets": [...] }, { "name": "HDMI-1", "widgets": [...] }]
}
+16
View File
@@ -109,6 +109,22 @@ Singleton {
save();
}
// Set a usage count directly (used for migration/merging)
function recordLauncherUsageMerge(key, count) {
let counts = Object.assign({}, adapter.launcherUsage || {});
counts[key] = count;
adapter.launcherUsage = counts;
save();
}
// Remove a usage key (used for cleaning up legacy keys after migration)
function clearLauncherUsage(key) {
let counts = Object.assign({}, adapter.launcherUsage || {});
delete counts[key];
adapter.launcherUsage = counts;
save();
}
// Debounced save timer
Timer {
id: saveTimer
@@ -441,6 +441,15 @@ Variants {
}
}
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")
@@ -115,6 +115,35 @@ Item {
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;
@@ -554,6 +583,8 @@ Item {
internal.isScaling = true;
internal.initialScale = root.widgetScale;
internal.lastScale = root.widgetScale;
internal.initialWidth = root.width;
internal.initialHeight = root.height;
}
onPositionChanged: mouse => {
@@ -571,6 +602,8 @@ Item {
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;
@@ -585,6 +618,7 @@ Item {
});
internal.isScaling = false;
internal.operationType = "";
root.widgetScale = root.snapScaleToGrid(root.widgetScale);
internal.lastScale = root.widgetScale;
}
}
@@ -77,13 +77,18 @@ ShapePath {
readonly property bool shouldFlatten: bar ? ShapeCornerHelper.shouldFlatten(barWidth, barHeight, radius) : false
readonly property real effectiveRadius: shouldFlatten ? (bar ? ShapeCornerHelper.getFlattenedRadius(Math.min(barWidth, barHeight), radius) : 0) : radius
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
readonly property real _minR: 0.01
// Helper function for getting corner radius based on state
function getCornerRadius(cornerState) {
// State -1 = no radius (flat corner)
// State -1 = flat corner — use minimum safe radius instead of 0
// to prevent degenerate PathArc (zero displacement + zero radius)
if (cornerState === -1)
return 0;
// All other states use effectiveRadius
return effectiveRadius;
return _minR;
// All other states use effectiveRadius (clamped to safe minimum)
return Math.max(_minR, effectiveRadius);
}
// Per-corner multipliers and radii based on bar's corner states (handle null bar)
@@ -107,13 +112,14 @@ ShapePath {
// Mirrors PanelBackground.isRenderable — prevents CurveRenderer crash on zero-area paths.
readonly property bool isRenderable: bar !== null && shouldShow && (isFramed ? (screenWidth > 0 && screenHeight > 0) : (barWidth > 0 && barHeight > 0))
// Extend bar background beyond screen edges where both adjacent corners are flat,
// to prevent CurveRenderer antialiasing artifacts on screen-flush edges
// Edge overshoot: extend bar background beyond screen edges where both adjacent
// corners are flat (state -1) to prevent CurveRenderer antialiasing artifacts.
// Uses corner state checks instead of radius === 0 since flat corners now have _minR.
readonly property real screenEdgeOvershoot: 2
readonly property real topEdgeOvs: (!isFramed && shouldShow && tlRadius === 0 && trRadius === 0 && barMappedPos.y <= 0) ? -screenEdgeOvershoot : 0
readonly property real bottomEdgeOvs: (!isFramed && shouldShow && blRadius === 0 && brRadius === 0 && (barMappedPos.y + barHeight) >= screenHeight) ? screenEdgeOvershoot : 0
readonly property real leftEdgeOvs: (!isFramed && shouldShow && tlRadius === 0 && blRadius === 0 && barMappedPos.x <= 0) ? -screenEdgeOvershoot : 0
readonly property real rightEdgeOvs: (!isFramed && shouldShow && trRadius === 0 && brRadius === 0 && (barMappedPos.x + barWidth) >= screenWidth) ? screenEdgeOvershoot : 0
readonly property real topEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.topRightCornerState === -1 && barMappedPos.y <= 0) ? -screenEdgeOvershoot : 0
readonly property real bottomEdgeOvs: (!isFramed && shouldShow && bar && bar.bottomLeftCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.y + barHeight) >= screenHeight) ? screenEdgeOvershoot : 0
readonly property real leftEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.bottomLeftCornerState === -1 && barMappedPos.x <= 0) ? -screenEdgeOvershoot : 0
readonly property real rightEdgeOvs: (!isFramed && shouldShow && bar && bar.topRightCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.x + barWidth) >= screenWidth) ? screenEdgeOvershoot : 0
// Auto-hide opacity factor for background fade
property real opacityFactor: (bar && bar.isHidden) ? 0 : 1
@@ -135,7 +141,9 @@ ShapePath {
// all subsequent path elements form a valid non-degenerate off-screen square.
// Each edge is split between PathLine and PathArc so no arc has zero displacement,
// preventing CurveRenderer triangulation crashes on degenerate arcs.
startX: isRenderable ? (isFramed ? 0 : (barMappedPos.x + leftEdgeOvs + tlRadius * tlMultX)) : -0.75
// For framed mode the outer path is a full-screen rectangle; _minR offsets at each
// corner prevent zero-displacement zero-radius arcs that crash qTriangulate.
startX: isRenderable ? (isFramed ? _minR : (barMappedPos.x + leftEdgeOvs + tlRadius * tlMultX)) : -0.75
startY: isRenderable ? (isFramed ? 0 : (barMappedPos.y + topEdgeOvs)) : -1
// ========== PATH DEFINITION ==========
@@ -145,58 +153,58 @@ ShapePath {
// off-screen square with non-degenerate arcs so CurveRenderer never receives
// a zero-area, bare-moveto, or zero-displacement arc path.
PathLine {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.trRadius * root.trMultX)) : 0
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.trRadius * root.trMultX)) : 0
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
}
// Bar top-right corner (only if not framed)
// Top-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs + root.trRadius * root.trMultY)) : -0.75
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.trRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.trRadius) : 0
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.trRadius * root.trMultY)) : -0.75
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.brRadius * root.brMultY)) : 0
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.brRadius * root.brMultY)) : 0
}
// Bar bottom-right corner (only if not framed)
// Bottom-right corner
PathArc {
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.brRadius * root.brMultX)) : -0.25
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.brRadius * root.brMultX)) : -0.25
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.brRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.brRadius) : 0
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs + root.blRadius * root.blMultX)) : -1
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.blRadius * root.blMultX)) : -1
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
}
// Bar bottom-left corner (only if not framed)
// Bottom-left corner
PathArc {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.blRadius * root.blMultY)) : -0.25
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.blRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.blRadius) : 0
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.blRadius * root.blMultY)) : -0.25
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY)
}
PathLine {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs + root.tlRadius * root.tlMultY)) : -1
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.tlRadius * root.tlMultY)) : -1
}
// Bar top-left corner (only if not framed, back to start)
// Top-left corner (back to start)
PathArc {
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs + root.tlRadius * root.tlMultX)) : -0.75
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.tlRadius * root.tlMultX)) : -0.75
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.tlRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.tlRadius) : 0
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY)
}
@@ -59,13 +59,18 @@ ShapePath {
readonly property bool shouldFlatten: panelBg ? ShapeCornerHelper.shouldFlatten(panelWidth, panelHeight, radius) : false
readonly property real effectiveRadius: shouldFlatten ? ShapeCornerHelper.getFlattenedRadius(Math.min(panelWidth, panelHeight), radius) : radius
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
readonly property real _minR: 0.01
// Helper function for getting corner radius based on state
function getCornerRadius(cornerState) {
// State -1 = no radius (flat corner)
// State -1 = flat corner — use minimum safe radius instead of 0
// to prevent degenerate PathArc (zero displacement + zero radius)
if (cornerState === -1)
return 0;
// All other states use effectiveRadius
return effectiveRadius;
return _minR;
// All other states use effectiveRadius (clamped to safe minimum)
return Math.max(_minR, effectiveRadius);
}
// Per-corner multipliers and radii based on panelBg's corner states
+18 -2
View File
@@ -304,9 +304,20 @@ Rectangle {
// Sort by _score (higher = better match), items without _score go first
if (searchText.trim() !== "") {
const boostByUsage = Settings.data.appLauncher.sortByMostUsed;
allResults.sort((a, b) => {
const sa = a._score !== undefined ? a._score : 0;
const sb = b._score !== undefined ? b._score : 0;
let sa = a._score !== undefined ? a._score : 0;
let sb = b._score !== undefined ? b._score : 0;
// Boost scores for frequently used items from tracked providers
if (boostByUsage) {
if (a.provider && a.provider.trackUsage && a.usageKey)
sa += 100.0 * Math.log2(1 + ShellState.getLauncherUsageCount(a.usageKey));
if (b.provider && b.provider.trackUsage && b.usageKey)
sb += 100.0 * Math.log2(1 + ShellState.getLauncherUsageCount(b.usageKey));
}
return sb - sa;
});
}
@@ -468,6 +479,11 @@ Rectangle {
const item = results[selectedIndex];
const provider = item.provider || currentProvider;
// Track usage for providers that opt in (cross-provider "most used" tracking)
if (Settings.data.appLauncher.sortByMostUsed && provider && provider.trackUsage && item.usageKey) {
ShellState.recordLauncherUsage(item.usageKey);
}
// Check if auto-paste is enabled and provider/item supports it
if (Settings.data.appLauncher.autoPasteClipboard && provider && provider.supportsAutoPaste && item.autoPasteText) {
if (item.onAutoPaste)
@@ -14,6 +14,7 @@ Item {
property string supportedLayouts: "both"
property bool isDefaultProvider: true // This provider handles empty search
property bool ignoreDensity: false // Apps should scale with launcher density
property bool trackUsage: true // Track usage frequency for "most used" sorting
// Category support
property string selectedCategory: "all"
@@ -516,6 +517,7 @@ Item {
function createResultEntry(app, score) {
return {
"appId": getAppKey(app),
"usageKey": getAppKey(app),
"name": app.name || "Unknown",
"description": app.genericName || app.comment || "",
"icon": app.icon || "application-x-executable",
@@ -523,10 +525,6 @@ Item {
"_score": (score !== undefined ? score : 0),
"provider": root,
"onActivate": function () {
if (Settings.data.appLauncher.sortByMostUsed) {
root.recordUsage(app);
}
// Close the launcher/SmartPanel immediately without any animations.
// Ensures we are not preventing the future focusing of the app
launcher.closeImmediately();
@@ -641,8 +639,29 @@ Item {
return String(app && app.name ? app.name : "unknown");
}
// Returns the usage count for an app, checking both the canonical key (app.id)
// and the legacy command-based key. If a legacy key has usage but the canonical
// key doesn't, the counts are migrated automatically.
function getUsageCount(app) {
return ShellState.getLauncherUsageCount(getAppKey(app));
const key = getAppKey(app);
let count = ShellState.getLauncherUsageCount(key);
// Check for legacy command-based key if the primary key is the app ID
if (app && app.id && app.command && app.command.join) {
const legacyKey = app.command.join(" ");
if (legacyKey !== key) {
const legacyCount = ShellState.getLauncherUsageCount(legacyKey);
if (legacyCount > 0) {
// Migrate: merge legacy count into the canonical key
count += legacyCount;
ShellState.recordLauncherUsageMerge(key, count);
ShellState.clearLauncherUsage(legacyKey);
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" (${legacyCount}) "${key}" (${count})`);
}
}
}
return count;
}
function recordUsage(app) {