add noctalia and fuzzel
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Notification
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
// Bar Component
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// This property will be set by MainScreen
|
||||
property ShellScreen screen: null
|
||||
|
||||
// Filter widgets to only include those that exist in the registry
|
||||
// This prevents errors when plugins are missing or widgets are being cleaned up
|
||||
function filterValidWidgets(widgets: list<var>): list<var> {
|
||||
if (!widgets)
|
||||
return [];
|
||||
return widgets.filter(function (w) {
|
||||
return w && w.id && BarWidgetRegistry.hasWidget(w.id);
|
||||
});
|
||||
}
|
||||
|
||||
// Hot corner: trigger click on first widget in a section
|
||||
function triggerFirstWidgetInSection(sectionName: string) {
|
||||
var widgets = BarService.getWidgetsBySection(sectionName, screen?.name);
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
var widget = widgets[i];
|
||||
if (widget && widget.visible && widget.widgetId !== "Spacer") {
|
||||
if (typeof widget.clicked === "function") {
|
||||
widget.clicked();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hot corner: trigger click on last widget in a section
|
||||
function triggerLastWidgetInSection(sectionName: string) {
|
||||
var widgets = BarService.getWidgetsBySection(sectionName, screen?.name);
|
||||
for (var i = widgets.length - 1; i >= 0; i--) {
|
||||
var widget = widgets[i];
|
||||
if (widget && widget.visible && widget.widgetId !== "Spacer") {
|
||||
if (typeof widget.clicked === "function") {
|
||||
widget.clicked();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose bar region for click-through mask
|
||||
readonly property var barRegion: barContentLoader.item?.children[0] || null
|
||||
|
||||
// Expose the actual bar Item for unified background system
|
||||
readonly property var barItem: barRegion
|
||||
|
||||
// Bar positioning properties (per-screen)
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool barFloating: Settings.data.bar.barType === "floating"
|
||||
|
||||
// Bar density (per-screen)
|
||||
readonly property string barDensity: Settings.getBarDensityForScreen(screen?.name)
|
||||
|
||||
// Bar sizing based on per-screen density
|
||||
readonly property real barHeight: Style.getBarHeightForDensity(barDensity, barIsVertical)
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForDensity(barDensity, barHeight)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForDensity(barHeight, capsuleHeight, barIsVertical)
|
||||
|
||||
// Bar widgets (per-screen) - initial configuration
|
||||
// Note: Updates are handled via Connections to BarService.widgetsRevisionChanged
|
||||
readonly property var barWidgets: Settings.getBarWidgetsForScreen(screen?.name)
|
||||
|
||||
// Stable ListModels for each section - prevents Repeater recreation on settings changes
|
||||
property ListModel leftWidgetsModel: ListModel {}
|
||||
property ListModel centerWidgetsModel: ListModel {}
|
||||
property ListModel rightWidgetsModel: ListModel {}
|
||||
|
||||
// Guard: set when Bar is destroyed; prevents Qt.callLater callbacks from running
|
||||
// during/after teardown (avoids SIGSEGV in QV4::Object::insertMember when rapid
|
||||
// workspace switch causes load/unload overlap with async widget incubation)
|
||||
property bool _destroyed: false
|
||||
Component.onDestruction: root._destroyed = true
|
||||
|
||||
// Sync a ListModel with widget data, preserving delegates when only settings change
|
||||
function syncWidgetModel(model, newWidgets) {
|
||||
var validWidgets = filterValidWidgets(newWidgets);
|
||||
|
||||
// Build list of current IDs in model
|
||||
var currentIds = [];
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
currentIds.push(model.get(i).id);
|
||||
}
|
||||
|
||||
// Build list of new IDs
|
||||
var newIds = validWidgets.map(w => w.id);
|
||||
|
||||
// Check if structure changed (different IDs or order)
|
||||
var structureChanged = currentIds.length !== newIds.length;
|
||||
if (!structureChanged) {
|
||||
for (var i = 0; i < currentIds.length; i++) {
|
||||
if (currentIds[i] !== newIds[i]) {
|
||||
structureChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d("Bar", "syncWidgetModel:", currentIds.join("|"), "→", newIds.join("|"), "changed:", structureChanged);
|
||||
|
||||
if (structureChanged) {
|
||||
// Rebuild model - IDs changed
|
||||
model.clear();
|
||||
for (var i = 0; i < validWidgets.length; i++) {
|
||||
model.append(validWidgets[i]);
|
||||
}
|
||||
}
|
||||
// If structure didn't change, delegates are preserved and will read fresh settings
|
||||
}
|
||||
|
||||
// Sync models when widget revision changes
|
||||
// Note: We use Connections instead of onBarWidgetsChanged because getBarWidgetsForScreen
|
||||
// returns the same object reference (Settings.data.bar.widgets) even when content changes,
|
||||
// so QML won't detect the change via property binding.
|
||||
Connections {
|
||||
target: BarService
|
||||
function onWidgetsRevisionChanged() {
|
||||
Logger.d("Bar", "onWidgetsRevisionChanged, revision:", BarService.widgetsRevision, "screen:", root.screen?.name);
|
||||
Qt.callLater(root._syncFromRevision);
|
||||
}
|
||||
}
|
||||
|
||||
function _syncFromRevision() {
|
||||
if (root._destroyed)
|
||||
return;
|
||||
var widgets = Settings.getBarWidgetsForScreen(screen?.name);
|
||||
if (widgets) {
|
||||
syncWidgetModel(leftWidgetsModel, widgets.left);
|
||||
syncWidgetModel(centerWidgetsModel, widgets.center);
|
||||
syncWidgetModel(rightWidgetsModel, widgets.right);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize models — deferred to next event-loop tick via Qt.callLater to avoid
|
||||
// re-entrant incubation: Component.onCompleted fires during QQmlObjectCreator::finalize,
|
||||
// and ListModel.append synchronously creates Repeater delegates whose own finalization
|
||||
// can corrupt the V4 heap (SIGSEGV in QV4::Object::insertMember).
|
||||
Component.onCompleted: {
|
||||
Logger.d("Bar", "Bar Component.onCompleted for screen:", screen?.name);
|
||||
Qt.callLater(root._initModels);
|
||||
}
|
||||
|
||||
function _initModels() {
|
||||
if (root._destroyed)
|
||||
return;
|
||||
var widgets = Settings.getBarWidgetsForScreen(screen?.name);
|
||||
if (widgets) {
|
||||
syncWidgetModel(leftWidgetsModel, widgets.left);
|
||||
syncWidgetModel(centerWidgetsModel, widgets.center);
|
||||
syncWidgetModel(rightWidgetsModel, widgets.right);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the parent (the Loader)
|
||||
anchors.fill: parent
|
||||
|
||||
// Register bar when screen becomes available
|
||||
onScreenChanged: {
|
||||
if (screen && screen.name) {
|
||||
Logger.d("Bar", "Bar screen set to:", screen.name);
|
||||
Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating);
|
||||
BarService.registerBar(screen.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for screen to be set before loading bar content
|
||||
Loader {
|
||||
id: barContentLoader
|
||||
anchors.fill: parent
|
||||
active: {
|
||||
if (root.screen === null || root.screen === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var monitors = Settings.data.bar.monitors || [];
|
||||
var result = monitors.length === 0 || monitors.includes(root.screen.name);
|
||||
return result;
|
||||
}
|
||||
|
||||
sourceComponent: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
// Bar container - Content
|
||||
Item {
|
||||
id: bar
|
||||
|
||||
// Wheel scroll handling (empty bar area)
|
||||
property int barWheelAccumulatedDelta: 0
|
||||
property bool barWheelCooldown: false
|
||||
readonly property string barWheelAction: {
|
||||
return Settings.data.bar.mouseWheelAction || "none";
|
||||
}
|
||||
readonly property string barRightClickAction: Settings.data.bar.rightClickAction || "controlCenter"
|
||||
|
||||
// Position and size the bar content based on orientation
|
||||
x: (root.barPosition === "right") ? (parent.width - root.barHeight) : 0
|
||||
y: (root.barPosition === "bottom") ? (parent.height - root.barHeight) : 0
|
||||
width: root.barIsVertical ? root.barHeight : parent.width
|
||||
height: root.barIsVertical ? parent.height : root.barHeight
|
||||
|
||||
// Corner states for new unified background system
|
||||
// State -1: No radius (flat/square corner)
|
||||
// State 0: Normal (inner curve)
|
||||
// State 1: Horizontal inversion (outer curve on X-axis)
|
||||
// State 2: Vertical inversion (outer curve on Y-axis)
|
||||
readonly property int topLeftCornerState: {
|
||||
// Floating bar: always simple rounded corners
|
||||
if (barFloating)
|
||||
return 0;
|
||||
// Top bar: top corners against screen edge = no radius
|
||||
if (barPosition === "top")
|
||||
return -1;
|
||||
// Left bar: top-left against screen edge = no radius
|
||||
if (barPosition === "left")
|
||||
return -1;
|
||||
// Bottom/Right bar with outerCorners: inverted corner
|
||||
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) {
|
||||
return barIsVertical ? 1 : 2; // horizontal invert for vertical bars, vertical invert for horizontal
|
||||
}
|
||||
// No outerCorners = square
|
||||
return -1;
|
||||
}
|
||||
|
||||
readonly property int topRightCornerState: {
|
||||
// Floating bar: always simple rounded corners
|
||||
if (barFloating)
|
||||
return 0;
|
||||
// Top bar: top corners against screen edge = no radius
|
||||
if (barPosition === "top")
|
||||
return -1;
|
||||
// Right bar: top-right against screen edge = no radius
|
||||
if (barPosition === "right")
|
||||
return -1;
|
||||
// Bottom/Left bar with outerCorners: inverted corner
|
||||
if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) {
|
||||
return barIsVertical ? 1 : 2;
|
||||
}
|
||||
// No outerCorners = square
|
||||
return -1;
|
||||
}
|
||||
|
||||
readonly property int bottomLeftCornerState: {
|
||||
// Floating bar: always simple rounded corners
|
||||
if (barFloating)
|
||||
return 0;
|
||||
// Bottom bar: bottom corners against screen edge = no radius
|
||||
if (barPosition === "bottom")
|
||||
return -1;
|
||||
// Left bar: bottom-left against screen edge = no radius
|
||||
if (barPosition === "left")
|
||||
return -1;
|
||||
// Top/Right bar with outerCorners: inverted corner
|
||||
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) {
|
||||
return barIsVertical ? 1 : 2;
|
||||
}
|
||||
// No outerCorners = square
|
||||
return -1;
|
||||
}
|
||||
|
||||
readonly property int bottomRightCornerState: {
|
||||
// Floating bar: always simple rounded corners
|
||||
if (barFloating)
|
||||
return 0;
|
||||
// Bottom bar: bottom corners against screen edge = no radius
|
||||
if (barPosition === "bottom")
|
||||
return -1;
|
||||
// Right bar: bottom-right against screen edge = no radius
|
||||
if (barPosition === "right")
|
||||
return -1;
|
||||
// Top/Left bar with outerCorners: inverted corner
|
||||
if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) {
|
||||
return barIsVertical ? 1 : 2;
|
||||
}
|
||||
// No outerCorners = square
|
||||
return -1;
|
||||
}
|
||||
|
||||
function isPointOverWidget(xPos, yPos) {
|
||||
var widgets = BarService.getAllWidgetInstances(null, screen.name);
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
var widget = widgets[i];
|
||||
if (!widget || !widget.visible || widget.widgetId === "Spacer") {
|
||||
continue;
|
||||
}
|
||||
var localPos = mapToItem(widget, xPos, yPos);
|
||||
|
||||
if (root.barIsVertical) {
|
||||
if (localPos.y >= -Style.marginS && localPos.y <= widget.height + Style.marginS) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (localPos.x >= -Style.marginS && localPos.x <= widget.width + Style.marginS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function switchWorkspaceByOffset(offset) {
|
||||
if (!root.screen || CompositorService.workspaces.count === 0)
|
||||
return;
|
||||
|
||||
var screenName = root.screen.name.toLowerCase();
|
||||
var candidates = [];
|
||||
for (var i = 0; i < CompositorService.workspaces.count; i++) {
|
||||
var ws = CompositorService.workspaces.get(i);
|
||||
var matchesScreen = CompositorService.globalWorkspaces || (ws.output && ws.output.toLowerCase() === screenName);
|
||||
if (matchesScreen)
|
||||
candidates.push(ws);
|
||||
}
|
||||
|
||||
if (candidates.length <= 1)
|
||||
return;
|
||||
|
||||
var current = -1;
|
||||
for (var j = 0; j < candidates.length; j++) {
|
||||
if (candidates[j].isFocused) {
|
||||
current = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (current < 0)
|
||||
current = 0;
|
||||
|
||||
var next = current + offset;
|
||||
if (Settings.data.bar.mouseWheelWrap) {
|
||||
next = next % candidates.length;
|
||||
if (next < 0)
|
||||
next = candidates.length - 1;
|
||||
} else {
|
||||
if (next < 0 || next >= candidates.length)
|
||||
return;
|
||||
}
|
||||
|
||||
if (next === current)
|
||||
return;
|
||||
CompositorService.switchToWorkspace(candidates[next]);
|
||||
}
|
||||
|
||||
function handleEmptyBarClick(action, followMouse, command, mouse) {
|
||||
if (action === "none")
|
||||
return;
|
||||
if (action === "controlCenter") {
|
||||
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
|
||||
controlCenterPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : "ControlCenter");
|
||||
mouse.accepted = true;
|
||||
} else if (action === "settings") {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
|
||||
settingsPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : null);
|
||||
mouse.accepted = true;
|
||||
} else if (action === "launcherPanel") {
|
||||
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
|
||||
launcherPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : null);
|
||||
mouse.accepted = true;
|
||||
} else if (action === "command") {
|
||||
runCustomCommand(command);
|
||||
mouse.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
function runCustomCommand(command) {
|
||||
if (!command || command.trim() === "")
|
||||
return;
|
||||
|
||||
const processString = "import QtQuick; import Quickshell.Io; Process { command: [\"sh\", \"-lc\", \"\"] }";
|
||||
|
||||
try {
|
||||
const processObj = Qt.createQmlObject(processString, root, "BarCommandProcess_" + Date.now());
|
||||
processObj.command = ["sh", "-lc", command];
|
||||
|
||||
processObj.exited.connect(function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
ToastService.showError(I18n.tr("toast.custom-command-failed.title"), I18n.tr("toast.custom-command-failed.description", {
|
||||
command: command,
|
||||
code: exitCode
|
||||
}));
|
||||
}
|
||||
processObj.destroy();
|
||||
});
|
||||
|
||||
processObj.running = true;
|
||||
} catch (e) {
|
||||
Logger.e("Bar", "Failed to start custom command:", e);
|
||||
ToastService.showError(I18n.tr("toast.custom-command-failed.title"), I18n.tr("toast.custom-command-failed.description", {
|
||||
command: command,
|
||||
code: "start_error"
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton | Qt.MiddleButton
|
||||
enabled: bar.barRightClickAction !== "none" || Settings.data.bar.middleClickAction !== "none"
|
||||
hoverEnabled: false
|
||||
preventStealing: true
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (bar.isPointOverWidget(mouse.x, mouse.y))
|
||||
return;
|
||||
bar.handleEmptyBarClick(bar.barRightClickAction, Settings.data.bar.rightClickFollowMouse, Settings.data.bar.rightClickCommand, mouse);
|
||||
return;
|
||||
}
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
if (bar.isPointOverWidget(mouse.x, mouse.y))
|
||||
return;
|
||||
bar.handleEmptyBarClick(Settings.data.bar.middleClickAction || "none", Settings.data.bar.middleClickFollowMouse, Settings.data.bar.middleClickCommand, mouse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce timer for wheel interactions
|
||||
Timer {
|
||||
id: barWheelDebounce
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
bar.barWheelCooldown = false;
|
||||
bar.barWheelAccumulatedDelta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll on empty bar area action
|
||||
WheelHandler {
|
||||
id: barWheelHandler
|
||||
target: bar
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
enabled: bar.barWheelAction !== "none"
|
||||
|
||||
onWheel: function (event) {
|
||||
if (bar.isPointOverWidget(event.x, event.y))
|
||||
return;
|
||||
|
||||
var dy = event.angleDelta.y;
|
||||
var dx = event.angleDelta.x;
|
||||
var useDy = Math.abs(dy) >= Math.abs(dx);
|
||||
var delta = useDy ? dy : dx;
|
||||
var step = 120;
|
||||
|
||||
if (bar.barWheelAction === "volume") {
|
||||
if (Settings.data.bar.reverseScroll)
|
||||
delta *= -1;
|
||||
|
||||
bar.barWheelAccumulatedDelta += delta;
|
||||
if (bar.barWheelAccumulatedDelta >= step) {
|
||||
AudioService.increaseVolume();
|
||||
bar.barWheelAccumulatedDelta = 0;
|
||||
event.accepted = true;
|
||||
} else if (bar.barWheelAccumulatedDelta <= -step) {
|
||||
AudioService.decreaseVolume();
|
||||
bar.barWheelAccumulatedDelta = 0;
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bar.barWheelCooldown)
|
||||
return;
|
||||
|
||||
bar.barWheelAccumulatedDelta += delta;
|
||||
if (Math.abs(bar.barWheelAccumulatedDelta) >= step) {
|
||||
var direction = bar.barWheelAccumulatedDelta > 0 ? -1 : 1;
|
||||
if (Settings.data.bar.reverseScroll)
|
||||
direction *= -1;
|
||||
if (bar.barWheelAction === "workspace") {
|
||||
bar.switchWorkspaceByOffset(direction);
|
||||
} else if (bar.barWheelAction === "content") {
|
||||
CompositorService.scrollWorkspaceContent(direction);
|
||||
}
|
||||
bar.barWheelCooldown = true;
|
||||
barWheelDebounce.restart();
|
||||
bar.barWheelAccumulatedDelta = 0;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.barIsVertical ? verticalBarComponent : horizontalBarComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For vertical bars
|
||||
Component {
|
||||
id: verticalBarComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
// Top edge hot corner - triggers first widget in left (top) section
|
||||
MouseArea {
|
||||
width: parent.width
|
||||
height: Style.marginS
|
||||
x: 0
|
||||
y: 0
|
||||
onClicked: root.triggerFirstWidgetInSection("left")
|
||||
}
|
||||
|
||||
// Bottom edge hot corner - triggers last widget in right (bottom) section
|
||||
MouseArea {
|
||||
width: parent.width
|
||||
height: Style.marginS
|
||||
x: 0
|
||||
anchors.bottom: parent.bottom
|
||||
onClicked: root.triggerLastWidgetInSection("right")
|
||||
}
|
||||
|
||||
// Calculate margin to center widgets vertically within the bar height
|
||||
readonly property real verticalBarMargin: Math.round((root.barHeight - root.capsuleHeight) / 2)
|
||||
|
||||
// Top section (left widgets)
|
||||
ColumnLayout {
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: verticalBarMargin + Settings.data.bar.contentPadding
|
||||
spacing: Settings.data.bar.widgetSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.leftWidgetsModel
|
||||
delegate: BarWidgetLoader {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
widgetId: model.id || ""
|
||||
widgetScreen: root.screen
|
||||
widgetProps: ({
|
||||
"widgetId": model.id,
|
||||
"section": "left",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": root.leftWidgetsModel.count
|
||||
})
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center section (center widgets)
|
||||
ColumnLayout {
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Settings.data.bar.widgetSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.centerWidgetsModel
|
||||
delegate: BarWidgetLoader {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
widgetId: model.id || ""
|
||||
widgetScreen: root.screen
|
||||
widgetProps: ({
|
||||
"widgetId": model.id,
|
||||
"section": "center",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": root.centerWidgetsModel.count
|
||||
})
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom section (right widgets)
|
||||
ColumnLayout {
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: verticalBarMargin + Settings.data.bar.contentPadding
|
||||
spacing: Settings.data.bar.widgetSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.rightWidgetsModel
|
||||
delegate: BarWidgetLoader {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
widgetId: model.id || ""
|
||||
widgetScreen: root.screen
|
||||
widgetProps: ({
|
||||
"widgetId": model.id,
|
||||
"section": "right",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": root.rightWidgetsModel.count
|
||||
})
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For horizontal bars
|
||||
Component {
|
||||
id: horizontalBarComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
// Left edge hot corner - triggers first widget in left section
|
||||
MouseArea {
|
||||
width: Style.marginS
|
||||
height: parent.height
|
||||
x: 0
|
||||
y: 0
|
||||
onClicked: root.triggerFirstWidgetInSection("left")
|
||||
}
|
||||
|
||||
// Right edge hot corner - triggers last widget in right section
|
||||
MouseArea {
|
||||
width: Style.marginS
|
||||
height: parent.height
|
||||
anchors.right: parent.right
|
||||
y: 0
|
||||
onClicked: root.triggerLastWidgetInSection("right")
|
||||
}
|
||||
|
||||
// Calculate margin to center widgets horizontally within the bar height
|
||||
readonly property real horizontalBarMargin: Math.round((root.barHeight - root.capsuleHeight) / 2)
|
||||
|
||||
// Left Section
|
||||
RowLayout {
|
||||
id: leftSection
|
||||
objectName: "leftSection"
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: horizontalBarMargin + Settings.data.bar.contentPadding
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
spacing: Settings.data.bar.widgetSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.leftWidgetsModel
|
||||
delegate: BarWidgetLoader {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
widgetId: model.id || ""
|
||||
widgetScreen: root.screen
|
||||
widgetProps: ({
|
||||
"widgetId": model.id,
|
||||
"section": "left",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": root.leftWidgetsModel.count
|
||||
})
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Center Section
|
||||
RowLayout {
|
||||
id: centerSection
|
||||
objectName: "centerSection"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
spacing: Settings.data.bar.widgetSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.centerWidgetsModel
|
||||
delegate: BarWidgetLoader {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
widgetId: model.id || ""
|
||||
widgetScreen: root.screen
|
||||
widgetProps: ({
|
||||
"widgetId": model.id,
|
||||
"section": "center",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": root.centerWidgetsModel.count
|
||||
})
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right Section
|
||||
RowLayout {
|
||||
id: rightSection
|
||||
objectName: "rightSection"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: horizontalBarMargin + Settings.data.bar.contentPadding
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
spacing: Settings.data.bar.widgetSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.rightWidgetsModel
|
||||
delegate: BarWidgetLoader {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
widgetId: model.id || ""
|
||||
widgetScreen: root.screen
|
||||
widgetProps: ({
|
||||
"widgetId": model.id,
|
||||
"section": "right",
|
||||
"sectionWidgetIndex": index,
|
||||
"sectionWidgetsCount": root.rightWidgetsModel.count
|
||||
})
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
|
||||
/**
|
||||
* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar
|
||||
*
|
||||
* This is a minimal window that works with the compositor to reserve space,
|
||||
* while the actual bar UI is rendered in NFullScreenWindow.
|
||||
*/
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool barFloating: Settings.data.bar.barType === "floating"
|
||||
readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal : 0
|
||||
readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical : 0
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(screen?.name)
|
||||
|
||||
// Invisible - just reserves space
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {}
|
||||
|
||||
// Wayland layer shell configuration
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "noctalia-bar-exclusion-" + (screen?.name || "unknown")
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Auto
|
||||
|
||||
// Anchor based on bar position
|
||||
anchors {
|
||||
top: barPosition === "top"
|
||||
bottom: barPosition === "bottom"
|
||||
left: barPosition === "left" || barPosition === "top" || barPosition === "bottom"
|
||||
right: barPosition === "right" || barPosition === "top" || barPosition === "bottom"
|
||||
}
|
||||
|
||||
// Size based on bar orientation
|
||||
// When floating, only reserve space for the bar + margin on the anchored edge
|
||||
implicitWidth: {
|
||||
if (barIsVertical) {
|
||||
// Vertical bar: reserve bar height + margin on the anchored edge only
|
||||
if (barFloating) {
|
||||
// For left bar, reserve left margin; for right bar, reserve right margin
|
||||
return barHeight + barMarginH;
|
||||
}
|
||||
return barHeight;
|
||||
}
|
||||
return 0; // Auto-width when left/right anchors are true
|
||||
}
|
||||
|
||||
implicitHeight: {
|
||||
if (!barIsVertical) {
|
||||
// Horizontal bar: reserve bar height + margin on the anchored edge only
|
||||
if (barFloating) {
|
||||
// For top bar, reserve top margin; for bottom bar, reserve bottom margin
|
||||
return barHeight + barMarginV;
|
||||
}
|
||||
return barHeight;
|
||||
}
|
||||
return 0; // Auto-height when top/bottom anchors are true
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.d("BarExclusionZone", "Created for screen:", screen?.name);
|
||||
Logger.d("BarExclusionZone", " Position:", barPosition, "Floating:", barFloating);
|
||||
Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right);
|
||||
Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property var tooltipText
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool oppositeDirection: false
|
||||
property bool hovered: false
|
||||
property bool rotateText: false
|
||||
property color customBackgroundColor: "transparent"
|
||||
property color customTextIconColor: "transparent"
|
||||
property color customIconColor: "transparent"
|
||||
property color customTextColor: "transparent"
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Size based on content for the content dimension, fill parent for the extended dimension
|
||||
// Horizontal bars: width = content, height = fill parent (for extended click area)
|
||||
// Vertical bars: width = fill parent, height = content
|
||||
width: isVerticalBar ? parent.width : (pillLoader.item ? pillLoader.item.implicitWidth : 0)
|
||||
height: isVerticalBar ? (pillLoader.item ? pillLoader.item.implicitHeight : 0) : parent.height
|
||||
implicitWidth: pillLoader.item ? pillLoader.item.implicitWidth : 0
|
||||
implicitHeight: pillLoader.item ? pillLoader.item.implicitHeight : 0
|
||||
|
||||
// Loader fills BarPill so child components can extend to full bar dimension
|
||||
Loader {
|
||||
id: pillLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
|
||||
|
||||
Component {
|
||||
id: verticalPillComponent
|
||||
BarPillVertical {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
oppositeDirection: root.oppositeDirection
|
||||
hovered: root.hovered
|
||||
rotateText: root.rotateText
|
||||
customBackgroundColor: root.customBackgroundColor
|
||||
customTextIconColor: root.customTextIconColor
|
||||
customIconColor: root.customIconColor
|
||||
customTextColor: root.customTextColor
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalPillComponent
|
||||
BarPillHorizontal {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
oppositeDirection: root.oppositeDirection
|
||||
hovered: root.hovered
|
||||
customBackgroundColor: root.customBackgroundColor
|
||||
customTextIconColor: root.customTextIconColor
|
||||
customIconColor: root.customIconColor
|
||||
customTextColor: root.customTextColor
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (pillLoader.item && pillLoader.item.show) {
|
||||
pillLoader.item.show();
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (pillLoader.item && pillLoader.item.hide) {
|
||||
pillLoader.item.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (pillLoader.item && pillLoader.item.showDelayed) {
|
||||
pillLoader.item.showDelayed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property var tooltipText
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool oppositeDirection: false
|
||||
property bool hovered: false
|
||||
property color customBackgroundColor: "transparent"
|
||||
property color customTextIconColor: "transparent"
|
||||
property color customIconColor: "transparent"
|
||||
property color customTextColor: "transparent"
|
||||
|
||||
readonly property bool collapseToIcon: forceClose && !forceOpen
|
||||
|
||||
// Effective shown state (true if hovered/animated open or forced)
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
readonly property bool hasIcon: root.icon !== ""
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
readonly property int pillHeight: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screen?.name)
|
||||
readonly property int pillPaddingHorizontal: Math.round(pillHeight * 0.2)
|
||||
readonly property int pillOverlap: Math.round(pillHeight * 0.5)
|
||||
readonly property int pillMaxWidth: Math.max(1, Math.round(textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap))
|
||||
|
||||
// Always prioritize hover color, then the custom one and finally the fallback color
|
||||
readonly property color bgColor: hovered ? Color.mHover : (customBackgroundColor.a > 0) ? customBackgroundColor : Style.capsuleColor
|
||||
readonly property color fgColor: hovered ? Color.mOnHover : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color iconFgColor: hovered ? Color.mOnHover : (customIconColor.a > 0) ? customIconColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color textFgColor: hovered ? Color.mOnHover : (customTextColor.a > 0) ? customTextColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
|
||||
readonly property real iconSize: Style.toOdd(pillHeight * 0.48)
|
||||
|
||||
// Content width calculation (for implicit sizing)
|
||||
readonly property real contentWidth: {
|
||||
if (collapseToIcon) {
|
||||
return hasIcon ? pillHeight : 0;
|
||||
}
|
||||
var overlap = hasIcon ? pillOverlap : 0;
|
||||
var baseWidth = hasIcon ? pillHeight : 0;
|
||||
return baseWidth + Math.max(0, pill.width - overlap);
|
||||
}
|
||||
|
||||
// Fill parent to extend click area to full bar height
|
||||
// Visual content is centered vertically within
|
||||
anchors.fill: parent
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: pillHeight
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onTooltipTextChanged() {
|
||||
if (hovered) {
|
||||
TooltipService.updateText(root.tooltipText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unified background for the entire pill area to avoid overlapping opacity
|
||||
Rectangle {
|
||||
id: pillBackground
|
||||
width: collapseToIcon ? pillHeight : root.width
|
||||
height: pillHeight
|
||||
radius: Style.radiusM
|
||||
color: root.bgColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
width: revealed ? pillMaxWidth : 1
|
||||
height: pillHeight
|
||||
|
||||
x: {
|
||||
if (!hasIcon)
|
||||
return 0;
|
||||
return oppositeDirection ? (iconCircle.x + iconCircle.width / 2) : (iconCircle.x + iconCircle.width / 2) - width;
|
||||
}
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: "transparent" // Make pill background transparent to avoid double opacity
|
||||
|
||||
topLeftRadius: oppositeDirection ? 0 : Style.radiusM
|
||||
bottomLeftRadius: oppositeDirection ? 0 : Style.radiusM
|
||||
topRightRadius: oppositeDirection ? Style.radiusM : 0
|
||||
bottomRightRadius: oppositeDirection ? Style.radiusM : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: {
|
||||
if (!hasIcon)
|
||||
return (parent.width - width) / 2;
|
||||
|
||||
// Better text horizontal centering
|
||||
var centerX = (parent.width - width) / 2;
|
||||
var offset = oppositeDirection ? Style.marginXS : -Style.marginXS;
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS;
|
||||
}
|
||||
return centerX + offset;
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: root.barFontSize
|
||||
applyUiScale: false
|
||||
color: root.textFgColor
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: hasIcon ? pillHeight : 0
|
||||
height: pillHeight
|
||||
radius: Math.min(Style.radiusL, width / 2)
|
||||
color: "transparent" // Make icon background transparent to avoid double opacity
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
x: oppositeDirection ? 0 : (parent.width - width)
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
color: root.iconFgColor
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: pillMaxWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true;
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start();
|
||||
root.shown();
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: pillMaxWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false;
|
||||
shouldAnimateHide = false;
|
||||
root.hidden();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
cursorShape: root.clicked ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onEntered: {
|
||||
hovered = true;
|
||||
root.entered();
|
||||
TooltipService.show(root, root.tooltipText, BarService.getTooltipDirection(root.screen?.name), (forceOpen || forceClose) ? Style.tooltipDelay : Style.tooltipDelayLong);
|
||||
if (forceClose) {
|
||||
return;
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false;
|
||||
root.exited();
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide();
|
||||
}
|
||||
TooltipService.hide();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
TooltipService.hide();
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked();
|
||||
}
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showAnim.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (collapseToIcon)
|
||||
return;
|
||||
if (forceOpen) {
|
||||
return;
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start();
|
||||
}
|
||||
showTimer.stop();
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showTimer.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop();
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.stop();
|
||||
showPill = true;
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property var tooltipText
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool oppositeDirection: false
|
||||
property bool hovered: false
|
||||
property bool rotateText: false
|
||||
property color customBackgroundColor: "transparent"
|
||||
property color customTextIconColor: "transparent"
|
||||
property color customIconColor: "transparent"
|
||||
property color customTextColor: "transparent"
|
||||
|
||||
readonly property bool collapseToIcon: forceClose && !forceOpen
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Sizing logic for vertical bars
|
||||
readonly property int buttonSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screen?.name)
|
||||
readonly property int pillHeight: buttonSize
|
||||
readonly property int pillOverlap: Math.round(buttonSize * 0.5)
|
||||
readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + Style.margin2M)) : buttonSize
|
||||
readonly property int maxPillHeight: rotateText ? Math.max(1, Math.round(textItem.implicitWidth + Style.margin2M + Math.round(iconCircle.height / 4))) : Math.max(1, Math.round(textItem.implicitHeight + Style.margin2M))
|
||||
|
||||
// Determine pill direction based on section position
|
||||
readonly property bool openDownward: oppositeDirection
|
||||
readonly property bool openUpward: !oppositeDirection
|
||||
|
||||
// Effective shown state (true if animated open or forced, but not if force closed)
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
readonly property bool hasIcon: root.icon !== ""
|
||||
|
||||
// Always prioritize hover color, then the custom one and finally the fallback color
|
||||
readonly property color bgColor: hovered ? Color.mHover : (customBackgroundColor.a > 0) ? customBackgroundColor : Style.capsuleColor
|
||||
readonly property color fgColor: hovered ? Color.mOnHover : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color iconFgColor: hovered ? Color.mOnHover : (customIconColor.a > 0) ? customIconColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color textFgColor: hovered ? Color.mOnHover : (customTextColor.a > 0) ? customTextColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
|
||||
readonly property real iconSize: Style.toOdd(pillHeight * 0.48)
|
||||
|
||||
// Content height calculation (for implicit sizing)
|
||||
readonly property real contentHeight: {
|
||||
if (collapseToIcon) {
|
||||
return hasIcon ? buttonSize : 0;
|
||||
}
|
||||
var overlap = hasIcon ? pillOverlap : 0;
|
||||
var baseHeight = hasIcon ? buttonSize : 0;
|
||||
return baseHeight + Math.max(0, pill.height - overlap);
|
||||
}
|
||||
|
||||
// Fill parent width to extend horizontal click area
|
||||
// Keep content-based height for visual layout
|
||||
anchors.left: parent ? parent.left : undefined
|
||||
anchors.right: parent ? parent.right : undefined
|
||||
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
height: contentHeight
|
||||
implicitWidth: buttonSize
|
||||
implicitHeight: contentHeight
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onTooltipTextChanged() {
|
||||
if (hovered) {
|
||||
TooltipService.updateText(root.tooltipText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unified background for the entire pill area to avoid overlapping opacity
|
||||
Rectangle {
|
||||
id: pillBackground
|
||||
width: buttonSize
|
||||
height: root.contentHeight
|
||||
radius: Style.radiusM
|
||||
color: root.bgColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: revealed ? maxPillHeight : 1
|
||||
|
||||
// Position based on direction - center the pill relative to the icon
|
||||
x: 0
|
||||
y: {
|
||||
if (!hasIcon)
|
||||
return 0;
|
||||
return openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2);
|
||||
}
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: "transparent" // Make pill background transparent to avoid double opacity
|
||||
|
||||
// Radius logic for vertical expansion - rounded on the side that connects to icon
|
||||
topLeftRadius: openUpward ? Style.radiusM : 0
|
||||
bottomLeftRadius: openDownward ? Style.radiusM : 0
|
||||
topRightRadius: openUpward ? Style.radiusM : 0
|
||||
bottomRightRadius: openDownward ? Style.radiusM : 0
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: hasIcon ? (openDownward ? Style.marginXXS : -Style.marginXXS) : 0
|
||||
rotation: rotateText ? -90 : 0
|
||||
text: root.text + root.suffix
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: root.barFontSize
|
||||
applyUiScale: false
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: root.textFgColor
|
||||
visible: revealed
|
||||
|
||||
function getVerticalCenterOffset() {
|
||||
// A small, symmetrical offset to push the text slightly away from the icon's edge.
|
||||
return openDownward ? Style.marginXS : -Style.marginXS;
|
||||
}
|
||||
}
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: buttonSize
|
||||
height: buttonSize
|
||||
radius: Math.min(Style.radiusL, width / 2)
|
||||
color: "transparent" // Make icon background transparent to avoid double opacity
|
||||
|
||||
// Icon positioning based on direction
|
||||
x: 0
|
||||
y: openUpward ? (root.contentHeight - height) : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
color: root.iconFgColor
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: 1
|
||||
to: maxPillHeight
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true;
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start();
|
||||
root.shown();
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: maxPillHeight
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false;
|
||||
shouldAnimateHide = false;
|
||||
root.hidden();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
cursorShape: root.clicked ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onEntered: {
|
||||
hovered = true;
|
||||
root.entered();
|
||||
TooltipService.show(root, root.tooltipText, BarService.getTooltipDirection(root.screen?.name), (forceOpen || forceClose) ? Style.tooltipDelay : Style.tooltipDelayLong);
|
||||
if (forceClose) {
|
||||
return;
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false;
|
||||
root.exited();
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide();
|
||||
}
|
||||
TooltipService.hide();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
TooltipService.hide();
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked();
|
||||
}
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showAnim.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (collapseToIcon)
|
||||
return;
|
||||
if (forceOpen) {
|
||||
return;
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start();
|
||||
}
|
||||
showTimer.stop();
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showTimer.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop();
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.stop();
|
||||
showPill = true;
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string widgetId
|
||||
required property var widgetScreen
|
||||
required property var widgetProps
|
||||
|
||||
// Extract section info from widgetProps
|
||||
readonly property string section: widgetProps ? (widgetProps.section || "") : ""
|
||||
readonly property int sectionIndex: widgetProps ? (widgetProps.sectionWidgetIndex || 0) : 0
|
||||
|
||||
// Store registration key at registration time so unregistration always uses the correct key,
|
||||
// even if binding properties (section, sectionIndex) have changed by destruction time
|
||||
property string _regScreen: ""
|
||||
property string _regSection: ""
|
||||
property string _regWidgetId: ""
|
||||
property int _regIndex: -1
|
||||
|
||||
function _unregister() {
|
||||
if (_regScreen !== "") {
|
||||
BarService.unregisterWidget(_regScreen, _regSection, _regWidgetId, _regIndex);
|
||||
_regScreen = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Bar orientation and height for extended click areas
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(widgetScreen?.name)
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(widgetScreen?.name)
|
||||
|
||||
// Request full bar dimension from layout to extend click areas above/below widgets
|
||||
// For horizontal bars: full bar height, widget's content width
|
||||
// For vertical bars: full bar width, widget's content height
|
||||
implicitWidth: isVerticalBar ? barHeight : getImplicitSize(loader.item, "implicitWidth")
|
||||
implicitHeight: isVerticalBar ? getImplicitSize(loader.item, "implicitHeight") : barHeight
|
||||
|
||||
// Remove layout space left by hidden widgets
|
||||
visible: loader.item ? ((loader.item.opacity > 0.0) || (loader.item.hasOwnProperty("hideMode") && loader.item.hideMode === "transparent")) : false
|
||||
|
||||
function getImplicitSize(item, prop) {
|
||||
return (item && item.visible) ? Math.round(item[prop]) : 0;
|
||||
}
|
||||
|
||||
// Only load if widget exists in registry
|
||||
function checkWidgetExists(): bool {
|
||||
return root.widgetId !== "" && BarWidgetRegistry.hasWidget(root.widgetId);
|
||||
}
|
||||
|
||||
// Force reload counter - incremented when plugin widget registry changes
|
||||
property int reloadCounter: 0
|
||||
|
||||
// Listen for plugin widget registry changes to force reload
|
||||
Connections {
|
||||
target: BarWidgetRegistry
|
||||
enabled: BarWidgetRegistry.isPluginWidget(root.widgetId)
|
||||
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
if (BarWidgetRegistry.hasWidget(root.widgetId)) {
|
||||
root.reloadCounter++;
|
||||
// Plugin widgets use setSource, so also trigger reload directly
|
||||
if (root._isPlugin && loader.active)
|
||||
root._loadWidget();
|
||||
Logger.d("BarWidgetLoader", "Plugin widget registry updated, reloading:", root.widgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool _isPlugin: BarWidgetRegistry.isPluginWidget(widgetId)
|
||||
|
||||
// Build initial properties that must be available during Component.onCompleted.
|
||||
// This prevents registration-key mismatches in widgets that build IDs from
|
||||
// screen.name, section, or sectionWidgetIndex.
|
||||
// All standard bar widget props are passed for both core and plugin widgets.
|
||||
// Plugins that don't define some of these properties will get harmless warnings
|
||||
// but still load correctly — and plugins that DO define them (e.g. for unique
|
||||
// SpectrumService keys with multiple instances) get correct values from the start.
|
||||
function _initialProps() {
|
||||
return {
|
||||
"screen": widgetScreen,
|
||||
"widgetId": widgetProps.widgetId || "",
|
||||
"section": widgetProps.section || "",
|
||||
"sectionWidgetIndex": widgetProps.sectionWidgetIndex || 0,
|
||||
"sectionWidgetsCount": widgetProps.sectionWidgetsCount || 0
|
||||
};
|
||||
}
|
||||
|
||||
// Core widget URLs: file names match widget IDs exactly
|
||||
readonly property string _barWidgetsDir: Quickshell.shellDir + "/Modules/Bar/Widgets/"
|
||||
|
||||
function _loadWidget() {
|
||||
if (!BarWidgetRegistry.hasWidget(root.widgetId))
|
||||
return;
|
||||
|
||||
var props = _initialProps();
|
||||
|
||||
if (_isPlugin) {
|
||||
var comp = BarWidgetRegistry.getWidget(root.widgetId);
|
||||
if (!comp)
|
||||
return;
|
||||
var pluginId = root.widgetId.replace("plugin:", "");
|
||||
var api = PluginService.getPluginAPI(pluginId);
|
||||
if (api)
|
||||
props.pluginApi = api;
|
||||
loader.setSource(comp.url, props);
|
||||
} else {
|
||||
loader.setSource(_barWidgetsDir + root.widgetId + ".qml", props);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
active: root.checkWidgetExists() && (root.reloadCounter >= 0)
|
||||
|
||||
// All widgets use setSource() so that screen and widget properties
|
||||
// are set as initial properties, available during Component.onCompleted.
|
||||
Component.onCompleted: root._loadWidget()
|
||||
|
||||
onActiveChanged: {
|
||||
if (active)
|
||||
root._loadWidget();
|
||||
}
|
||||
|
||||
// Unregister when the loaded item is destroyed (Loader deactivated or sourceComponent changed)
|
||||
onItemChanged: {
|
||||
if (!item) {
|
||||
root._unregister();
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name);
|
||||
|
||||
// Extend widget to fill full bar dimension for extended click areas
|
||||
// For horizontal bars: widget fills bar height (content width preserved)
|
||||
// For vertical bars: widget fills bar width (content height preserved)
|
||||
if (root.isVerticalBar) {
|
||||
item.width = Qt.binding(function () {
|
||||
return root.barHeight;
|
||||
});
|
||||
} else {
|
||||
item.height = Qt.binding(function () {
|
||||
return root.barHeight;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply remaining widget properties (screen is already set as initial prop)
|
||||
for (var prop in widgetProps) {
|
||||
if (item.hasOwnProperty(prop)) {
|
||||
item[prop] = widgetProps[prop];
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister any previous registration before registering the new instance
|
||||
root._unregister();
|
||||
|
||||
// Register and store the key for reliable unregistration
|
||||
BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item);
|
||||
root._regScreen = widgetScreen.name;
|
||||
root._regSection = section;
|
||||
root._regWidgetId = widgetId;
|
||||
root._regIndex = sectionIndex;
|
||||
|
||||
// Call custom onLoaded if it exists
|
||||
if (item.hasOwnProperty("onLoaded")) {
|
||||
item.onLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
root._unregister();
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
Component.onCompleted: {
|
||||
if (!BarWidgetRegistry.hasWidget(widgetId)) {
|
||||
Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property var trayItem: null
|
||||
property var anchorItem: null
|
||||
property real anchorX
|
||||
property real anchorY
|
||||
property bool isSubMenu: false
|
||||
property string widgetSection: ""
|
||||
property int widgetIndex: -1
|
||||
|
||||
// Derive menu from trayItem (only used for non-submenus)
|
||||
readonly property QsMenuHandle menu: isSubMenu ? null : (trayItem ? trayItem.menu : null)
|
||||
|
||||
// Compute if current tray item is pinned
|
||||
readonly property bool isPinned: {
|
||||
if (!trayItem || widgetSection === "" || widgetIndex < 0)
|
||||
return false;
|
||||
var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length)
|
||||
return false;
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings || widgetSettings.id !== "Tray")
|
||||
return false;
|
||||
var pinnedList = widgetSettings.pinned || [];
|
||||
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
|
||||
for (var i = 0; i < pinnedList.length; i++) {
|
||||
if (pinnedList[i] === itemName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
readonly property int menuWidth: 220
|
||||
|
||||
implicitWidth: menuWidth
|
||||
|
||||
// Use the content height of the Flickable for implicit height
|
||||
implicitHeight: Math.min(screen?.height * 0.9, flickable.contentHeight + Style.margin2S)
|
||||
|
||||
// When implicitHeight changes (menu content loads), force anchor recalculation
|
||||
onImplicitHeightChanged: {
|
||||
if (visible && anchorItem) {
|
||||
Qt.callLater(() => {
|
||||
anchor.updateAnchor();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
visible: false
|
||||
color: "transparent"
|
||||
anchor.item: anchorItem
|
||||
anchor.rect.x: {
|
||||
if (anchorItem && screen) {
|
||||
let baseX = anchorX;
|
||||
|
||||
// Calculate position relative to current screen
|
||||
let menuScreenX;
|
||||
if (isSubMenu && anchorItem.Window && anchorItem.Window.window) {
|
||||
const posInPopup = anchorItem.mapToItem(null, 0, 0);
|
||||
const parentWindow = anchorItem.Window.window;
|
||||
const windowXOnScreen = parentWindow.x - screen.x;
|
||||
menuScreenX = windowXOnScreen + posInPopup.x + baseX;
|
||||
} else {
|
||||
const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0);
|
||||
const anchorScreenX = anchorGlobalPos.x;
|
||||
menuScreenX = anchorScreenX + baseX;
|
||||
}
|
||||
|
||||
const menuRight = menuScreenX + implicitWidth;
|
||||
const screenRight = screen.width;
|
||||
const menuLeft = menuScreenX;
|
||||
|
||||
// Only adjust if menu would clip off screen boundaries
|
||||
// Don't adjust if the positioning is intentional (e.g., negative offset for right bar)
|
||||
if (menuRight > screenRight && menuLeft < screenRight) {
|
||||
// Clipping on right edge - shift left
|
||||
const overflow = menuRight - screenRight;
|
||||
return baseX - overflow - Style.marginS;
|
||||
} else if (menuLeft < 0 && menuRight > 0) {
|
||||
// Clipping on left edge - shift right
|
||||
return baseX - menuLeft + Style.marginS;
|
||||
}
|
||||
|
||||
return baseX;
|
||||
}
|
||||
return anchorX;
|
||||
}
|
||||
anchor.rect.y: {
|
||||
if (anchorItem && screen) {
|
||||
const barPosition = Settings.getBarPositionForScreen(root.screen?.name);
|
||||
|
||||
let baseY = anchorY;
|
||||
|
||||
// Only apply bottom bar special positioning if:
|
||||
// 1. Not a submenu
|
||||
// 2. Bar is at bottom
|
||||
// 3. anchorY is not already negative (if negative, it's pre-calculated from drawer)
|
||||
const shouldApplyBottomBarLogic = !isSubMenu && barPosition === "bottom" && anchorY >= 0;
|
||||
|
||||
if (shouldApplyBottomBarLogic) {
|
||||
// For bottom bar from the bar itself, position menu above the anchor with margin
|
||||
baseY = -(implicitHeight + Style.marginS);
|
||||
} else if (barPosition === "top" && !isSubMenu && anchorY >= 0) {
|
||||
// For top bar: position menu below bar with margin
|
||||
const barHeight = Style.getBarHeightForScreen(root.screen?.name);
|
||||
baseY = barHeight + Style.marginS;
|
||||
}
|
||||
|
||||
// Use a robust way to get screen coordinates
|
||||
const posInWindow = anchorItem.mapToItem(null, 0, 0);
|
||||
const parentWindow = anchorItem.Window.window;
|
||||
|
||||
// Calculate screen-relative Y of the window
|
||||
let windowYOnScreen = (parentWindow && screen) ? (parentWindow.y - screen.y) : 0;
|
||||
|
||||
// If window reported 0 but bar is at bottom, assume it's at screen bottom
|
||||
if (windowYOnScreen === 0 && barPosition === "bottom" && screen) {
|
||||
windowYOnScreen = screen.height - (parentWindow ? parentWindow.height : Style.getBarHeightForScreen(screen.name));
|
||||
}
|
||||
|
||||
// Calculate the screen Y of the menu top
|
||||
// Use a small guess for height if implicitHeight is 0 to avoid covering the bar on the first frame
|
||||
const effectiveHeight = implicitHeight > 0 ? implicitHeight : 200;
|
||||
const effectiveBaseY = shouldApplyBottomBarLogic ? -(effectiveHeight + Style.marginS) : baseY;
|
||||
|
||||
const menuScreenY = windowYOnScreen + posInWindow.y + effectiveBaseY;
|
||||
const menuBottom = menuScreenY + (implicitHeight > 0 ? implicitHeight : effectiveHeight);
|
||||
const screenHeight = screen ? screen.height : 1080;
|
||||
|
||||
// Adjust the final baseY (the actual value returned to anchor.rect.y)
|
||||
let finalBaseY = shouldApplyBottomBarLogic ? -(implicitHeight + Style.marginS) : baseY;
|
||||
|
||||
// Adjust if menu would clip off the bottom
|
||||
if (menuBottom > screenHeight) {
|
||||
const overflow = menuBottom - screenHeight;
|
||||
finalBaseY -= (overflow + Style.marginS);
|
||||
}
|
||||
|
||||
// Adjust if menu would clip off the top
|
||||
// menuScreenY < 0 means it's above the screen edge
|
||||
if (menuScreenY < 0) {
|
||||
finalBaseY -= (menuScreenY - Style.marginS);
|
||||
}
|
||||
|
||||
return finalBaseY;
|
||||
}
|
||||
|
||||
// Fallback if no anchor/screen
|
||||
if (isSubMenu) {
|
||||
return anchorY;
|
||||
}
|
||||
return anchorY + (Settings.getBarPositionForScreen(root.screen?.name) === "bottom" ? -implicitHeight : Style.getBarHeightForScreen(root.screen?.name));
|
||||
}
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
Logger.w("TrayMenu", "anchorItem is undefined, won't show menu.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opener.children || opener.children.values.length === 0) {
|
||||
//Logger.w("TrayMenu", "Menu not ready, delaying show")
|
||||
Qt.callLater(() => showAt(item, x, y));
|
||||
return;
|
||||
}
|
||||
|
||||
anchorItem = item;
|
||||
anchorX = x;
|
||||
anchorY = y;
|
||||
|
||||
visible = true;
|
||||
forceActiveFocus();
|
||||
|
||||
// Force update after showing.
|
||||
Qt.callLater(() => {
|
||||
root.anchor.updateAnchor();
|
||||
});
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
visible = false;
|
||||
|
||||
// Clean up all submenus recursively
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const child = columnLayout.children[i];
|
||||
if (child?.subMenu) {
|
||||
child.subMenu.hideMenu();
|
||||
child.subMenu.destroy();
|
||||
child.subMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: root.hideMenu()
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: root.menu
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS)
|
||||
radius: Style.radiusM
|
||||
|
||||
// Fade-in animation
|
||||
opacity: root.visible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
contentHeight: columnLayout.implicitHeight
|
||||
interactive: true
|
||||
|
||||
// Fade-in animation
|
||||
opacity: root.visible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Use a ColumnLayout to handle menu item arrangement
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
width: flickable.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: opener.children ? [...opener.children.values] : []
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: {
|
||||
if (modelData?.isSeparator) {
|
||||
return 8;
|
||||
} else {
|
||||
// Calculate based on text content
|
||||
const textHeight = text.contentHeight || (Style.fontSizeS * 1.2);
|
||||
return Math.max(28, textHeight + Style.margin2S);
|
||||
}
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
property var subMenu: null
|
||||
|
||||
NDivider {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.margin2M
|
||||
visible: modelData?.isSeparator ?? false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: innerRect
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Color.mHover : "transparent"
|
||||
radius: Style.radiusS
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Indicator Container
|
||||
Item {
|
||||
visible: (modelData?.buttonType ?? QsMenuButtonType.None) !== QsMenuButtonType.None
|
||||
|
||||
implicitWidth: Math.round(Style.baseWidgetSize * 0.5)
|
||||
implicitHeight: Math.round(Style.baseWidgetSize * 0.5)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Helper properties
|
||||
readonly property int type: modelData?.buttonType ?? QsMenuButtonType.None
|
||||
readonly property bool isRadio: type === QsMenuButtonType.RadioButton
|
||||
readonly property bool isChecked: modelData?.checkState === Qt.Checked || (modelData?.checked ?? false)
|
||||
|
||||
// Color Logic
|
||||
readonly property color activeColor: mouseArea.containsMouse ? Color.mOnHover : Color.mPrimary
|
||||
readonly property color checkMarkColor: mouseArea.containsMouse ? Color.mHover : Color.mOnPrimary
|
||||
readonly property color borderColor: isChecked ? activeColor : (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface)
|
||||
|
||||
// Checkbox Visuals
|
||||
Rectangle {
|
||||
visible: !parent.isRadio
|
||||
anchors.centerIn: parent
|
||||
width: Math.round(Style.baseWidgetSize * 0.5)
|
||||
height: Math.round(Style.baseWidgetSize * 0.5)
|
||||
radius: Style.iRadiusXS
|
||||
color: "transparent" // Transparent to match RadioButton style
|
||||
border.color: parent.borderColor
|
||||
border.width: Style.borderM
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: parent.parent.isChecked
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
icon: "check"
|
||||
color: parent.parent.activeColor
|
||||
pointSize: Math.max(Style.fontSizeXXS, parent.width * 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
// RadioButton Visuals
|
||||
Rectangle {
|
||||
visible: parent.isRadio
|
||||
anchors.centerIn: parent
|
||||
width: Style.toOdd(Style.baseWidgetSize * 0.5)
|
||||
height: Style.toOdd(Style.baseWidgetSize * 0.5)
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: parent.borderColor
|
||||
border.width: Style.borderM // Slightly thicker for radio look
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: parent.parent.isChecked
|
||||
anchors.centerIn: parent
|
||||
width: Style.toOdd(parent.width * 0.5)
|
||||
height: Style.toOdd(parent.height * 0.5)
|
||||
radius: width / 2
|
||||
color: parent.parent.activeColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: Style.marginL
|
||||
Layout.preferredHeight: Style.marginL
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
pointSize: Style.fontSizeS
|
||||
applyUiScale: false
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && root.visible
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (modelData && !modelData.isSeparator) {
|
||||
if (modelData.hasChildren) {
|
||||
// Click on items with children toggles submenu
|
||||
if (entry.subMenu) {
|
||||
// Close existing submenu
|
||||
entry.subMenu.hideMenu();
|
||||
entry.subMenu.destroy();
|
||||
entry.subMenu = null;
|
||||
} else {
|
||||
// Close any other open submenus first
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const sibling = columnLayout.children[i];
|
||||
if (sibling !== entry && sibling.subMenu) {
|
||||
sibling.subMenu.hideMenu();
|
||||
sibling.subMenu.destroy();
|
||||
sibling.subMenu = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine submenu opening direction
|
||||
let openLeft = false;
|
||||
const barPosition = Settings.getBarPositionForScreen(root.screen?.name);
|
||||
const globalPos = entry.mapToItem(null, 0, 0);
|
||||
|
||||
if (barPosition === "right") {
|
||||
openLeft = true;
|
||||
} else if (barPosition === "left") {
|
||||
openLeft = false;
|
||||
} else {
|
||||
openLeft = (root.widgetSection === "right");
|
||||
}
|
||||
|
||||
// Open new submenu
|
||||
entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, {
|
||||
"menu": modelData,
|
||||
"isSubMenu": true,
|
||||
"screen": root.screen
|
||||
});
|
||||
|
||||
if (entry.subMenu) {
|
||||
const overlap = 60;
|
||||
entry.subMenu.anchorItem = entry;
|
||||
entry.subMenu.anchorX = openLeft ? -overlap : overlap;
|
||||
entry.subMenu.anchorY = 0;
|
||||
entry.subMenu.visible = true;
|
||||
// Force anchor update with new position
|
||||
Qt.callLater(() => {
|
||||
entry.subMenu.anchor.updateAnchor();
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Click on regular items triggers them
|
||||
modelData.triggered();
|
||||
root.hideMenu();
|
||||
|
||||
// Close the drawer if it's open
|
||||
if (root.screen) {
|
||||
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
|
||||
if (panel && panel.visible) {
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (subMenu) {
|
||||
subMenu.destroy();
|
||||
subMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PIN / UNPIN
|
||||
Rectangle {
|
||||
visible: {
|
||||
if (widgetSection === "" || widgetIndex < 0)
|
||||
return false;
|
||||
var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length)
|
||||
return false;
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings)
|
||||
return false;
|
||||
return widgetSettings.drawerEnabled ?? false;
|
||||
}
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 28
|
||||
color: pinUnpinMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Qt.alpha(Color.mPrimary, 0.08)
|
||||
radius: Style.radiusS
|
||||
border.color: Qt.alpha(Color.mPrimary, pinUnpinMouseArea.containsMouse ? 0.4 : 0.2)
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: root.isPinned ? "unpin" : "pin"
|
||||
pointSize: Style.fontSizeS
|
||||
applyUiScale: false
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
color: Color.mPrimary
|
||||
text: root.isPinned ? I18n.tr("panels.bar.tray-unpin-application") : I18n.tr("panels.bar.tray-pin-application")
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinUnpinMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (root.isPinned) {
|
||||
root.removeFromPinned();
|
||||
} else {
|
||||
root.addToPinned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToPinned() {
|
||||
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
|
||||
Logger.w("TrayMenu", "Cannot pin: missing tray item or widget info");
|
||||
return;
|
||||
}
|
||||
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
|
||||
if (!itemName) {
|
||||
Logger.w("TrayMenu", "Cannot pin: tray item has no name");
|
||||
return;
|
||||
}
|
||||
var screenName = root.screen?.name || "";
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length) {
|
||||
Logger.w("TrayMenu", "Cannot pin: invalid widget index");
|
||||
return;
|
||||
}
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings || widgetSettings.id !== "Tray") {
|
||||
Logger.w("TrayMenu", "Cannot pin: widget is not a Tray widget");
|
||||
return;
|
||||
}
|
||||
var pinnedList = widgetSettings.pinned || [];
|
||||
var newPinned = pinnedList.slice();
|
||||
newPinned.push(itemName);
|
||||
var newSettings = Object.assign({}, widgetSettings);
|
||||
newSettings.pinned = newPinned;
|
||||
widgets[widgetIndex] = newSettings;
|
||||
|
||||
// Write to the correct location: screen override or global
|
||||
if (Settings.hasScreenOverride(screenName, "widgets")) {
|
||||
var overrideWidgets = Settings.getBarWidgetsForScreen(screenName);
|
||||
overrideWidgets[widgetSection] = widgets;
|
||||
Settings.setScreenOverride(screenName, "widgets", overrideWidgets);
|
||||
} else {
|
||||
Settings.data.bar.widgets[widgetSection] = widgets;
|
||||
}
|
||||
Settings.saveImmediate();
|
||||
|
||||
// Close drawer when pinning (drawer needs to resize)
|
||||
if (screen) {
|
||||
const panel = PanelService.getPanel("trayDrawerPanel", screen);
|
||||
if (panel)
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromPinned() {
|
||||
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
|
||||
Logger.w("TrayMenu", "Cannot unpin: missing tray item or widget info");
|
||||
return;
|
||||
}
|
||||
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
|
||||
if (!itemName) {
|
||||
Logger.w("TrayMenu", "Cannot unpin: tray item has no name");
|
||||
return;
|
||||
}
|
||||
var screenName = root.screen?.name || "";
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length) {
|
||||
Logger.w("TrayMenu", "Cannot unpin: invalid widget index");
|
||||
return;
|
||||
}
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings || widgetSettings.id !== "Tray") {
|
||||
Logger.w("TrayMenu", "Cannot unpin: widget is not a Tray widget");
|
||||
return;
|
||||
}
|
||||
var pinnedList = widgetSettings.pinned || [];
|
||||
var newPinned = [];
|
||||
for (var i = 0; i < pinnedList.length; i++) {
|
||||
if (pinnedList[i] !== itemName) {
|
||||
newPinned.push(pinnedList[i]);
|
||||
}
|
||||
}
|
||||
var newSettings = Object.assign({}, widgetSettings);
|
||||
newSettings.pinned = newPinned;
|
||||
widgets[widgetIndex] = newSettings;
|
||||
|
||||
// Write to the correct location: screen override or global
|
||||
if (Settings.hasScreenOverride(screenName, "widgets")) {
|
||||
var overrideWidgets = Settings.getBarWidgetsForScreen(screenName);
|
||||
overrideWidgets[widgetSection] = widgets;
|
||||
Settings.setScreenOverride(screenName, "widgets", overrideWidgets);
|
||||
} else {
|
||||
Settings.data.bar.widgets[widgetSection] = widgets;
|
||||
}
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: pillContainer
|
||||
|
||||
required property var workspace
|
||||
required property bool isVertical
|
||||
|
||||
// These must be provided by the parent Workspace widget
|
||||
required property real baseDimensionRatio
|
||||
required property real capsuleHeight
|
||||
required property real barHeight
|
||||
required property string labelMode
|
||||
required property int fontWeight
|
||||
required property int characterCount
|
||||
required property real textRatio
|
||||
required property bool showLabelsOnlyWhenOccupied
|
||||
required property string focusedColor
|
||||
required property string occupiedColor
|
||||
required property string emptyColor
|
||||
required property real masterProgress
|
||||
required property bool effectsActive
|
||||
required property color effectColor
|
||||
required property var getWorkspaceWidth
|
||||
required property var getWorkspaceHeight
|
||||
|
||||
// Fixed dimension (cross-axis) for visual pill
|
||||
readonly property real fixedDimension: Style.toOdd(capsuleHeight * baseDimensionRatio)
|
||||
|
||||
// Animated pill dimensions (for visual pill, not container)
|
||||
property real pillWidth: isVertical ? fixedDimension : getWorkspaceWidth(workspace, false)
|
||||
property real pillHeight: isVertical ? getWorkspaceHeight(workspace, false) : fixedDimension
|
||||
|
||||
// Container uses full barHeight on cross-axis for larger click area
|
||||
width: isVertical ? barHeight : getWorkspaceWidth(workspace, false)
|
||||
height: isVertical ? getWorkspaceHeight(workspace, false) : barHeight
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "active"
|
||||
when: workspace.isActive
|
||||
PropertyChanges {
|
||||
target: pillContainer
|
||||
width: isVertical ? barHeight : getWorkspaceWidth(workspace, true)
|
||||
height: isVertical ? getWorkspaceHeight(workspace, true) : barHeight
|
||||
pillWidth: isVertical ? fixedDimension : getWorkspaceWidth(workspace, true)
|
||||
pillHeight: isVertical ? getWorkspaceHeight(workspace, true) : fixedDimension
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "inactive"
|
||||
to: "active"
|
||||
NumberAnimation {
|
||||
properties: isVertical ? "height,pillHeight" : "width,pillWidth"
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: "inactive"
|
||||
NumberAnimation {
|
||||
properties: isVertical ? "height,pillHeight" : "width,pillWidth"
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: pillContainer.pillWidth
|
||||
height: pillContainer.pillHeight
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
radius: Style.radiusM
|
||||
z: 0
|
||||
|
||||
color: {
|
||||
if (pillMouseArea.containsMouse)
|
||||
return Color.mHover;
|
||||
if (workspace.isFocused)
|
||||
return Color.resolveColorKey(focusedColor);
|
||||
if (workspace.isUrgent)
|
||||
return Color.mError;
|
||||
if (workspace.isOccupied)
|
||||
return Color.resolveColorKey(occupiedColor);
|
||||
return Qt.alpha(Color.resolveColorKey(emptyColor), 0.3);
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: (labelMode !== "none") && (!showLabelsOnlyWhenOccupied || workspace.isOccupied || workspace.isFocused)
|
||||
anchors.fill: parent
|
||||
sourceComponent: Component {
|
||||
NText {
|
||||
text: {
|
||||
if (workspace.name && workspace.name.length > 0) {
|
||||
if (labelMode === "name") {
|
||||
return workspace.name.substring(0, characterCount);
|
||||
}
|
||||
if (labelMode === "index+name") {
|
||||
// Vertical mode: compact format (no space, first char only)
|
||||
// Horizontal mode: full format (space, more chars)
|
||||
if (isVertical) {
|
||||
return workspace.idx.toString() + workspace.name.substring(0, 1);
|
||||
}
|
||||
return workspace.idx.toString() + " " + workspace.name.substring(0, characterCount);
|
||||
}
|
||||
}
|
||||
return workspace.idx.toString();
|
||||
}
|
||||
family: Settings.data.ui.fontFixed
|
||||
// Size based on the fixed dimension (cross-axis) of the visual pill
|
||||
pointSize: (isVertical ? pillContainer.pillWidth : pillContainer.pillHeight) * textRatio
|
||||
applyUiScale: false
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.weight: fontWeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
if (pillMouseArea.containsMouse)
|
||||
return Color.mOnHover;
|
||||
if (workspace.isFocused)
|
||||
return Color.resolveOnColorKey(focusedColor);
|
||||
if (workspace.isUrgent)
|
||||
return Color.mOnError;
|
||||
if (workspace.isOccupied)
|
||||
return Color.resolveOnColorKey(occupiedColor);
|
||||
return Color.resolveOnColorKey(emptyColor);
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Material 3-inspired smooth animations
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on pillWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on pillHeight {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
// Full-height click area
|
||||
MouseArea {
|
||||
id: pillMouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
CompositorService.switchToWorkspace(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
// Burst effect overlay for focused pill
|
||||
Rectangle {
|
||||
id: pillBurst
|
||||
anchors.centerIn: pill
|
||||
width: pillContainer.pillWidth + 18 * masterProgress * scale
|
||||
height: pillContainer.pillHeight + 18 * masterProgress * scale
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: effectColor
|
||||
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - masterProgress))))
|
||||
opacity: effectsActive && workspace.isFocused ? (1.0 - masterProgress) * 0.7 : 0
|
||||
visible: effectsActive && workspace.isFocused
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
Layout.preferredHeight: isVerticalBar ? -1 : Style.getBarHeightForScreen(screenName)
|
||||
Layout.preferredWidth: isVerticalBar ? Style.getBarHeightForScreen(screenName) : -1
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: false
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] || {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length && widgets[sectionWidgetIndex]) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Widget settings - matching MediaMini pattern
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : (widgetMetadata.showIcon || false)
|
||||
readonly property bool showText: (widgetSettings.showText !== undefined) ? widgetSettings.showText : (widgetMetadata.showText || false)
|
||||
readonly property string hideMode: (widgetSettings.hideMode !== undefined) ? widgetSettings.hideMode : (widgetMetadata.hideMode || "hidden")
|
||||
readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode || "hover")
|
||||
|
||||
// Maximum widget width with user settings support
|
||||
readonly property real maxWidth: (widgetSettings.maxWidth !== undefined) ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth || 0, screen ? screen.width * 0.06 : 0)
|
||||
readonly property bool useFixedWidth: (widgetSettings.useFixedWidth !== undefined) ? widgetSettings.useFixedWidth : (widgetMetadata.useFixedWidth || false)
|
||||
readonly property string textColorKey: (widgetSettings.textColor !== undefined) ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
readonly property color textColor: Color.resolveColorKey(textColorKey)
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(screenName)
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
|
||||
readonly property bool hasFocusedWindow: CompositorService.getFocusedWindow() !== null
|
||||
readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window"
|
||||
readonly property string fallbackIcon: "user-desktop"
|
||||
|
||||
readonly property int iconSize: Style.toOdd(capsuleHeight * 0.75)
|
||||
readonly property int verticalSize: Style.toOdd(capsuleHeight * 0.85)
|
||||
|
||||
// For horizontal bars, height is always barHeight (no animation needed)
|
||||
// For vertical bars, collapse to 0 when hidden
|
||||
implicitHeight: isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : verticalSize) : barHeight
|
||||
implicitWidth: isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : verticalSize) : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth)
|
||||
|
||||
// "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty
|
||||
visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0
|
||||
opacity: ((hideMode !== "hidden" || hasFocusedWindow) && (hideMode !== "transparent" || hasFocusedWindow)) ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
function calculateContentWidth() {
|
||||
// Calculate the actual content width based on visible elements
|
||||
var contentWidth = 0;
|
||||
var margins = Style.margin2S; // Left and right margins
|
||||
|
||||
// Icon width (if visible)
|
||||
if (showIcon) {
|
||||
contentWidth += iconSize;
|
||||
if (showText) {
|
||||
contentWidth += Style.marginS; // Spacing after icon
|
||||
}
|
||||
}
|
||||
|
||||
// Text width (use the measured width)
|
||||
if (showText) {
|
||||
contentWidth += titleContainer.measuredWidth;
|
||||
// Additional small margin for text
|
||||
contentWidth += Style.margin2XXS;
|
||||
}
|
||||
|
||||
// Add container margins
|
||||
contentWidth += margins;
|
||||
|
||||
return Math.ceil(contentWidth);
|
||||
}
|
||||
|
||||
// Dynamic width: adapt to content but respect maximum width setting
|
||||
readonly property real dynamicWidth: {
|
||||
// If using fixed width mode, always use maxWidth
|
||||
if (useFixedWidth) {
|
||||
return maxWidth;
|
||||
}
|
||||
// Otherwise, adapt to content
|
||||
if (!hasFocusedWindow) {
|
||||
return Math.min(calculateContentWidth(), maxWidth);
|
||||
}
|
||||
// Use content width but don't exceed user-set maximum width
|
||||
return Math.min(calculateContentWidth(), maxWidth);
|
||||
}
|
||||
|
||||
function getAppIcon() {
|
||||
try {
|
||||
// Try CompositorService first
|
||||
const focusedWindow = CompositorService.getFocusedWindow();
|
||||
if (focusedWindow && focusedWindow.appId) {
|
||||
try {
|
||||
const idValue = focusedWindow.appId;
|
||||
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue);
|
||||
const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase());
|
||||
if (iconResult && iconResult !== "") {
|
||||
return iconResult;
|
||||
}
|
||||
} catch (iconError) {
|
||||
Logger.w("ActiveWindow", "Error getting icon from CompositorService:", iconError);
|
||||
}
|
||||
}
|
||||
|
||||
if (CompositorService.isHyprland) {
|
||||
// Fallback to ToplevelManager
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel) {
|
||||
try {
|
||||
const activeToplevel = ToplevelManager.activeToplevel;
|
||||
if (activeToplevel.appId) {
|
||||
const idValue2 = activeToplevel.appId;
|
||||
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2);
|
||||
const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase());
|
||||
if (iconResult2 && iconResult2 !== "") {
|
||||
return iconResult2;
|
||||
}
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
Logger.w("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ThemeIcons.iconFromName(fallbackIcon);
|
||||
} catch (e) {
|
||||
Logger.w("ActiveWindow", "Error in getAppIcon:", e);
|
||||
return ThemeIcons.iconFromName(fallbackIcon);
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: windowActiveRect
|
||||
visible: root.visible
|
||||
x: isVerticalBar ? Style.pixelAlignCenter(parent.width, width) : 0
|
||||
y: isVerticalBar ? 0 : Style.pixelAlignCenter(parent.height, height)
|
||||
width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth)
|
||||
height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : capsuleHeight
|
||||
radius: Style.radiusM
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
// Smooth width transition
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: isVerticalBar ? 0 : Style.marginS
|
||||
anchors.rightMargin: isVerticalBar ? 0 : Style.marginS
|
||||
|
||||
// Horizontal layout for top/bottom bars
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS
|
||||
visible: !isVerticalBar
|
||||
z: 1
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: iconSize
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: showIcon
|
||||
|
||||
IconImage {
|
||||
id: windowIcon
|
||||
anchors.fill: parent
|
||||
source: getAppIcon()
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Apply dock shader to active window icon (always themed)
|
||||
layer.enabled: widgetSettings.colorizeIcons !== false
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
|
||||
property real colorizeMode: 0.0 // Dock mode (grayscale)
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NScrollText {
|
||||
id: titleContainer
|
||||
text: windowTitle
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: root.capsuleHeight
|
||||
fadeRoundLeftCorners: !showIcon
|
||||
visible: showText
|
||||
|
||||
maxWidth: {
|
||||
// Calculate available width based on other elements
|
||||
var iconWidth = (showIcon && windowIcon.visible ? (iconSize + Style.marginS) : 0);
|
||||
var totalMargins = Style.margin2XXS;
|
||||
var availableWidth = mainContainer.width - iconWidth - totalMargins;
|
||||
return Math.max(20, availableWidth);
|
||||
}
|
||||
scrollMode: {
|
||||
if (scrollingMode === "always")
|
||||
return NScrollText.ScrollMode.Always;
|
||||
if (scrollingMode === "hover")
|
||||
return NScrollText.ScrollMode.Hover;
|
||||
return NScrollText.ScrollMode.Never;
|
||||
}
|
||||
forcedHover: mainMouseArea.containsMouse
|
||||
fadeExtent: 0.1
|
||||
fadeCornerRadius: Style.radiusM
|
||||
|
||||
NText {
|
||||
text: windowTitle
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: root.textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout for left/right bars - icon only
|
||||
Item {
|
||||
id: verticalLayout
|
||||
width: parent.width - Style.margin2M
|
||||
height: parent.height - Style.margin2M
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
visible: isVerticalBar
|
||||
z: 1
|
||||
|
||||
// Window icon
|
||||
Item {
|
||||
id: verticalIconContainer
|
||||
width: root.iconSize
|
||||
height: width
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
visible: windowTitle !== ""
|
||||
|
||||
IconImage {
|
||||
id: windowIconVertical
|
||||
anchors.fill: parent
|
||||
source: getAppIcon()
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
visible: source !== ""
|
||||
|
||||
// Apply dock shader to active window icon (always themed)
|
||||
layer.enabled: widgetSettings.colorizeIcons !== false
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: Color.mOnSurface
|
||||
property real colorizeMode: 0.0 // Dock mode (grayscale)
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area moved to root
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for hover detection
|
||||
MouseArea {
|
||||
id: mainMouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
// Extend click area to screen edge if widget is at the start/end
|
||||
anchors.leftMargin: (!isVerticalBar && section === "left" && sectionWidgetIndex === 0) ? -Style.marginS : 0
|
||||
anchors.rightMargin: (!isVerticalBar && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginS : 0
|
||||
anchors.topMargin: (isVerticalBar && section === "left" && sectionWidgetIndex === 0) ? -Style.marginM : 0
|
||||
anchors.bottomMargin: (isVerticalBar && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginM : 0
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onEntered: {
|
||||
if ((windowTitle !== "") && isVerticalBar || (scrollingMode === "never")) {
|
||||
TooltipService.show(root, windowTitle, BarService.getTooltipDirection(root.screen?.name));
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
TooltipService.hide();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onActiveWindowChanged() {
|
||||
try {
|
||||
windowIcon.source = Qt.binding(getAppIcon);
|
||||
windowIconVertical.source = Qt.binding(getAppIcon);
|
||||
} catch (e) {
|
||||
Logger.w("ActiveWindow", "Error in onActiveWindowChanged:", e);
|
||||
}
|
||||
}
|
||||
function onWindowListChanged() {
|
||||
try {
|
||||
windowIcon.source = Qt.binding(getAppIcon);
|
||||
windowIconVertical.source = Qt.binding(getAppIcon);
|
||||
} catch (e) {
|
||||
Logger.w("ActiveWindow", "Error in onWindowListChanged:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import qs.Widgets.AudioSpectrum
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
||||
readonly property int visualizerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
|
||||
readonly property bool hideWhenIdle: widgetSettings.hideWhenIdle !== undefined ? widgetSettings.hideWhenIdle : widgetMetadata.hideWhenIdle
|
||||
readonly property string colorName: widgetSettings.colorName !== undefined ? widgetSettings.colorName : widgetMetadata.colorName
|
||||
|
||||
readonly property color fillColor: Color.resolveColorKey(colorName)
|
||||
|
||||
readonly property bool shouldShow: (currentVisualizerType !== "" && currentVisualizerType !== "none") && (!hideWhenIdle || MediaService.isPlaying)
|
||||
|
||||
// Register/unregister with SpectrumService based on visibility (use screenName — screen can be null after DPMS/output changes)
|
||||
readonly property string spectrumComponentId: "bar:audiovisualizer:" + screenName + ":" + root.section + ":" + root.sectionWidgetIndex
|
||||
|
||||
onShouldShowChanged: {
|
||||
if (root.shouldShow) {
|
||||
SpectrumService.registerComponent(root.spectrumComponentId);
|
||||
} else {
|
||||
SpectrumService.unregisterComponent(root.spectrumComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (root.shouldShow) {
|
||||
SpectrumService.unregisterComponent(root.spectrumComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
// Content dimensions for implicit sizing
|
||||
readonly property real contentWidth: !shouldShow ? 0 : isVerticalBar ? capsuleHeight : visualizerWidth
|
||||
readonly property real contentHeight: !shouldShow ? 0 : isVerticalBar ? visualizerWidth : capsuleHeight
|
||||
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
visible: shouldShow
|
||||
opacity: shouldShow ? 1.0 : 0.0
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Store visualizer type to force re-evaluation
|
||||
readonly property string currentVisualizerType: Settings.data.audio.visualizerType
|
||||
|
||||
// Visual capsule centered in parent
|
||||
Rectangle {
|
||||
id: background
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
anchors.centerIn: parent
|
||||
radius: Style.radiusS
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
// When visualizer type or playback changes, shouldShow updates automatically
|
||||
// The Loader dynamically loads the appropriate visualizer based on settings
|
||||
Loader {
|
||||
id: visualizerLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
active: shouldShow
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: {
|
||||
switch (currentVisualizerType) {
|
||||
case "linear":
|
||||
return linearComponent;
|
||||
case "mirrored":
|
||||
return mirroredComponent;
|
||||
case "wave":
|
||||
return waveComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.cycle-visualizer"),
|
||||
"action": "cycle-visualizer",
|
||||
"icon": "chart-column"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
if (screen) {
|
||||
PanelService.closeContextMenu(screen);
|
||||
}
|
||||
|
||||
if (action === "cycle-visualizer") {
|
||||
const types = ["linear", "mirrored", "wave"];
|
||||
const currentIndex = types.indexOf(currentVisualizerType);
|
||||
const nextIndex = (currentIndex + 1) % types.length;
|
||||
Settings.data.audio.visualizerType = types[nextIndex];
|
||||
} else if (action === "widget-settings" && screen) {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click to cycle through visualizer types
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (screen) {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
} else {
|
||||
const types = ["linear", "mirrored", "wave"];
|
||||
const currentIndex = types.indexOf(currentVisualizerType);
|
||||
const nextIndex = (currentIndex + 1) % types.length;
|
||||
Settings.data.audio.visualizerType = types[nextIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: linearComponent
|
||||
NLinearSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: root.fillColor
|
||||
showMinimumSignal: true
|
||||
vertical: root.isVerticalBar
|
||||
barPosition: root.barPosition
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mirroredComponent
|
||||
NMirroredSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: root.fillColor
|
||||
showMinimumSignal: true
|
||||
vertical: root.isVerticalBar
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: waveComponent
|
||||
NWaveSpectrum {
|
||||
anchors.fill: parent
|
||||
values: SpectrumService.values
|
||||
fillColor: root.fillColor
|
||||
showMinimumSignal: true
|
||||
vertical: root.isVerticalBar
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
|
||||
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property bool useGraphicMode: displayMode === "graphic" || displayMode === "graphic-clean"
|
||||
|
||||
readonly property bool hideIfNotDetected: widgetSettings.hideIfNotDetected !== undefined ? widgetSettings.hideIfNotDetected : widgetMetadata.hideIfNotDetected
|
||||
readonly property bool hideIfIdle: widgetSettings.hideIfIdle !== undefined ? widgetSettings.hideIfIdle : widgetMetadata.hideIfIdle
|
||||
|
||||
// Check if selected device is actually present/connected
|
||||
readonly property bool isReady: BatteryService.isDeviceReady(selectedDevice)
|
||||
readonly property bool isPresent: BatteryService.isDevicePresent(selectedDevice)
|
||||
readonly property real percent: isReady ? BatteryService.getPercentage(selectedDevice) : -1
|
||||
readonly property bool isCharging: isReady ? BatteryService.isCharging(selectedDevice) : false
|
||||
readonly property bool isPluggedIn: isReady ? BatteryService.isPluggedIn(selectedDevice) : false
|
||||
readonly property bool isLowBattery: isReady ? BatteryService.isLowBattery(selectedDevice) : false
|
||||
readonly property bool isCriticalBattery: isReady ? BatteryService.isCriticalBattery(selectedDevice) : false
|
||||
|
||||
// Visibility: show if hideIfNotDetected is false, or if battery is ready
|
||||
readonly property bool shouldShow: !hideIfNotDetected || (isReady && (hideIfIdle ? !isPluggedIn : true))
|
||||
readonly property string deviceNativePath: widgetSettings.deviceNativePath !== undefined ? widgetSettings.deviceNativePath : widgetMetadata.deviceNativePath
|
||||
readonly property var selectedDevice: BatteryService.isDevicePresent(BatteryService.findDevice(deviceNativePath)) ? BatteryService.findDevice(deviceNativePath) : null
|
||||
|
||||
readonly property var tooltipContent: {
|
||||
if (!isReady || !isPresent) {
|
||||
return I18n.tr("battery.no-battery-detected");
|
||||
}
|
||||
|
||||
let rows = [];
|
||||
const isInternal = selectedDevice.isLaptopBattery;
|
||||
if (isInternal) {
|
||||
// Show charge percentage
|
||||
rows.push([I18n.tr("battery.battery-level"), `${percent}%`]);
|
||||
|
||||
let timeText = BatteryService.getTimeRemainingText(selectedDevice);
|
||||
if (timeText) {
|
||||
const colonIdx = timeText.indexOf(":");
|
||||
if (colonIdx >= 0) {
|
||||
rows.push([timeText.substring(0, colonIdx).trim(), timeText.substring(colonIdx + 1).trim()]);
|
||||
} else {
|
||||
rows.push([timeText, ""]);
|
||||
}
|
||||
}
|
||||
|
||||
let rateText = BatteryService.getRateText(selectedDevice);
|
||||
if (!isPluggedIn && rateText) {
|
||||
const colonIdx = rateText.indexOf(":");
|
||||
if (colonIdx >= 0) {
|
||||
rows.push([rateText.substring(0, colonIdx).trim(), rateText.substring(colonIdx + 1).trim()]);
|
||||
} else {
|
||||
rows.push([rateText, ""]);
|
||||
}
|
||||
}
|
||||
|
||||
// Show battery health if supported (check actual battery, not DisplayDevice)
|
||||
let healthDevice = selectedDevice.healthSupported ? selectedDevice : (BatteryService.laptopBatteries.length > 0 ? BatteryService.laptopBatteries[0] : null);
|
||||
if (healthDevice && healthDevice.healthSupported) {
|
||||
rows.push([I18n.tr("battery.battery-health"), `${Math.round(healthDevice.healthPercentage)}%`]);
|
||||
}
|
||||
} else if (selectedDevice) {
|
||||
// External / Peripheral Device (Phone, Keyboard, Mouse, Gamepad, Headphone etc.)
|
||||
let name = BatteryService.getDeviceName(selectedDevice);
|
||||
rows.push([name, `${percent}%`]);
|
||||
}
|
||||
|
||||
// If we are showing the main laptop battery, append external devices
|
||||
if (isInternal) {
|
||||
var external = BatteryService.bluetoothBatteries;
|
||||
if (external.length > 0) {
|
||||
if (rows.length > 0) {
|
||||
rows.push(["---", "---"]); // Separator
|
||||
}
|
||||
for (var j = 0; j < external.length; j++) {
|
||||
var dev = external[j];
|
||||
var dName = BatteryService.getDeviceName(dev);
|
||||
var dPct = BatteryService.getPercentage(dev);
|
||||
rows.push([dName, `${dPct}%`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
visible: shouldShow
|
||||
opacity: shouldShow ? 1.0 : 0.0
|
||||
|
||||
implicitWidth: useGraphicMode ? capsule.width : pill.width
|
||||
implicitHeight: useGraphicMode ? capsule.height : pill.height
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GRAPHIC MODE ====================
|
||||
|
||||
// Capsule background (graphic mode only)
|
||||
Rectangle {
|
||||
id: capsule
|
||||
visible: root.useGraphicMode
|
||||
anchors.centerIn: nBattery
|
||||
width: root.isBarVertical ? root.capsuleHeight : nBattery.width + Style.margin2S
|
||||
height: root.isBarVertical ? nBattery.height + Style.margin2S : root.capsuleHeight
|
||||
radius: Math.min(Style.radiusL, width / 2)
|
||||
color: graphicMouseArea.containsMouse ? Color.mHover : Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NBattery {
|
||||
id: nBattery
|
||||
visible: root.useGraphicMode
|
||||
anchors.centerIn: parent
|
||||
baseSize: (Style.getBarHeightForScreen(root.screenName) / root.capsuleHeight) * Style.fontSizeXXS
|
||||
showPercentageText: root.displayMode !== "graphic-clean"
|
||||
vertical: root.isBarVertical
|
||||
percentage: root.percent
|
||||
ready: root.isReady
|
||||
charging: root.isCharging
|
||||
pluggedIn: root.isPluggedIn
|
||||
low: root.isLowBattery
|
||||
critical: root.isCriticalBattery
|
||||
baseColor: graphicMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
|
||||
textColor: graphicMouseArea.containsMouse ? Color.mHover : Color.mSurface
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: graphicMouseArea
|
||||
visible: root.useGraphicMode
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: {
|
||||
if (!getBatteryPanel()?.isPanelOpen && root.tooltipContent) {
|
||||
TooltipService.show(root, root.tooltipContent, BarService.getTooltipDirection(root.screen?.name));
|
||||
tooltipRefreshTimer.start();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
tooltipRefreshTimer.stop();
|
||||
TooltipService.hide();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
TooltipService.hide();
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
PanelService.showContextMenu(contextMenu, nBattery, screen);
|
||||
} else {
|
||||
toggleBatteryPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: tooltipRefreshTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (graphicMouseArea.containsMouse) {
|
||||
TooltipService.updateText(root.tooltipContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ICON MODE ====================
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
visible: !root.useGraphicMode
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
icon: BatteryService.getIcon(root.percent, root.isCharging, root.isPluggedIn, root.isReady)
|
||||
text: root.isReady ? root.percent : "-"
|
||||
suffix: "%"
|
||||
autoHide: false
|
||||
forceOpen: root.isReady && root.displayMode === "icon-always"
|
||||
forceClose: root.displayMode === "icon-only" || !root.isReady
|
||||
customBackgroundColor: root.isCharging ? Color.mPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mError : "transparent")
|
||||
customTextIconColor: root.isCharging ? Color.mOnPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mOnError : "transparent")
|
||||
tooltipText: !getBatteryPanel()?.isPanelOpen ? root.tooltipContent : ""
|
||||
onClicked: toggleBatteryPanel()
|
||||
onRightClicked: PanelService.showContextMenu(contextMenu, pill, screen)
|
||||
}
|
||||
|
||||
// ==================== SHARED ====================
|
||||
|
||||
function getBatteryPanel() {
|
||||
var panel = PanelService.getPanel("batteryPanel", screen);
|
||||
if (panel) {
|
||||
panel.panelID = {
|
||||
showPowerProfiles: widgetSettings.showPowerProfiles !== undefined ? widgetSettings.showPowerProfiles : widgetMetadata.showPowerProfiles,
|
||||
showNoctaliaPerformance: widgetSettings.showNoctaliaPerformance !== undefined ? widgetSettings.showNoctaliaPerformance : widgetMetadata.showNoctaliaPerformance
|
||||
};
|
||||
}
|
||||
return panel;
|
||||
}
|
||||
|
||||
function toggleBatteryPanel() {
|
||||
getBatteryPanel()?.toggle(root);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings // For SettingsPanel
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": BluetoothService.enabled ? I18n.tr("actions.disable-bluetooth") : I18n.tr("actions.enable-bluetooth"),
|
||||
"action": "toggle-bluetooth",
|
||||
"icon": BluetoothService.enabled ? "bluetooth-off" : "bluetooth",
|
||||
"enabled": !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("common.bluetooth") + " " + I18n.tr("tooltips.open-settings"),
|
||||
"action": "bluetooth-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "toggle-bluetooth") {
|
||||
BluetoothService.setBluetoothEnabled(!BluetoothService.enabled);
|
||||
} else if (action === "bluetooth-settings") {
|
||||
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen);
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: !BluetoothService.enabled ? "bluetooth-off" : ((BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) ? "bluetooth-connected" : "bluetooth")
|
||||
text: {
|
||||
if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) {
|
||||
const firstDevice = BluetoothService.connectedDevices[0];
|
||||
return firstDevice.name || firstDevice.deviceName;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
suffix: {
|
||||
if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 1) {
|
||||
return ` + ${BluetoothService.connectedDevices.length - 1}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
autoHide: false
|
||||
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
|
||||
forceClose: isBarVertical || root.displayMode === "alwaysHide" || text === ""
|
||||
onClicked: {
|
||||
var p = PanelService.getPanel("bluetoothPanel", screen);
|
||||
if (p)
|
||||
p.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("bluetoothPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
}
|
||||
if (pill.text !== "") {
|
||||
return pill.text;
|
||||
}
|
||||
return I18n.tr("tooltips.bluetooth-devices");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
readonly property bool applyToAllMonitors: widgetSettings.applyToAllMonitors !== undefined ? widgetSettings.applyToAllMonitors : (Settings.data.brightness.syncAllMonitors !== undefined ? Settings.data.brightness.syncAllMonitors : widgetMetadata.applyToAllMonitors)
|
||||
readonly property bool reverseScroll: Settings.data.general.reverseScroll
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstBrightnessReceived: false
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
// Track the brightness monitor reactively via declarative binding so it
|
||||
// updates atomically when monitors change, avoiding a transient undefined
|
||||
// state that occurs when Monitor QtObjects are destroyed before the
|
||||
// imperative updateMonitor() call would run.
|
||||
property var brightnessMonitor: {
|
||||
var _ = BrightnessService.monitors; // reactive dependency
|
||||
var __ = BrightnessService.ddcMonitors; // reactive dependency
|
||||
if (!screen)
|
||||
return null;
|
||||
return BrightnessService.getMonitorForScreen(screen) ?? null;
|
||||
}
|
||||
|
||||
function getControllableMonitorCount() {
|
||||
var monitors = BrightnessService.monitors || [];
|
||||
var count = 0;
|
||||
for (var i = 0; i < monitors.length; i++) {
|
||||
if (monitors[i] && monitors[i].brightnessControlAvailable)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
visible: brightnessMonitor !== null
|
||||
opacity: brightnessMonitor !== null ? 1.0 : 0.0
|
||||
|
||||
function getIcon() {
|
||||
var monitor = brightnessMonitor;
|
||||
if (!monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness))
|
||||
return "sun-off";
|
||||
var brightness = monitor.brightness;
|
||||
if (brightness <= 0.001)
|
||||
return "sun-off";
|
||||
return brightness <= 0.5 ? "brightness-low" : "brightness-high";
|
||||
}
|
||||
|
||||
// Connection used to open the pill when brightness changes
|
||||
Connections {
|
||||
target: brightnessMonitor
|
||||
ignoreUnknownSignals: true
|
||||
function onBrightnessUpdated() {
|
||||
// Ignore if this is the first time we receive an update.
|
||||
// Most likely service just kicked off.
|
||||
if (!firstBrightnessReceived) {
|
||||
firstBrightnessReceived = true;
|
||||
return;
|
||||
}
|
||||
|
||||
pill.show();
|
||||
hideTimerAfterChange.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimerAfterChange
|
||||
interval: 2500
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: pill.hide()
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.open-display-settings"),
|
||||
"action": "open-display-settings",
|
||||
"icon": "sun"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "open-display-settings") {
|
||||
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
|
||||
settingsPanel.requestedTab = SettingsPanel.Tab.Display;
|
||||
settingsPanel.open();
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: getIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: {
|
||||
var monitor = brightnessMonitor;
|
||||
if (!monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness))
|
||||
return "";
|
||||
return Math.round(monitor.brightness * 100);
|
||||
}
|
||||
suffix: text.length > 0 ? "%" : "-"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: {
|
||||
var monitor = brightnessMonitor;
|
||||
var panel = PanelService.getPanel("brightnessPanel", screen);
|
||||
if (panel?.isPanelOpen || !monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness))
|
||||
return "";
|
||||
return I18n.tr("tooltips.brightness-at", {
|
||||
"brightness": Math.round(monitor.brightness * 100)
|
||||
});
|
||||
}
|
||||
|
||||
onWheel: function (angle) {
|
||||
var monitor = brightnessMonitor;
|
||||
if (!monitor || !monitor.brightnessControlAvailable)
|
||||
return;
|
||||
|
||||
if (root.reverseScroll)
|
||||
angle *= -1;
|
||||
|
||||
if (angle === 0)
|
||||
return;
|
||||
|
||||
var shouldApplyToAll = root.applyToAllMonitors && root.getControllableMonitorCount() > 1;
|
||||
if (shouldApplyToAll) {
|
||||
var direction = angle > 0 ? 1 : -1;
|
||||
var baseValue = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness;
|
||||
var step = monitor.stepSize;
|
||||
var minValue = monitor.minBrightnessValue;
|
||||
|
||||
if (direction > 0 && Settings.data.brightness.enforceMinimum && baseValue < minValue) {
|
||||
baseValue = Math.max(step, minValue);
|
||||
} else {
|
||||
baseValue = baseValue + direction * step;
|
||||
}
|
||||
|
||||
var targetValue = Math.max(minValue, Math.min(1, baseValue));
|
||||
|
||||
BrightnessService.monitors.forEach(function (m) {
|
||||
if (m && m.brightnessControlAvailable) {
|
||||
m.setBrightnessDebounced(targetValue);
|
||||
}
|
||||
});
|
||||
} else if (angle > 0) {
|
||||
monitor.increaseBrightness();
|
||||
} else if (angle < 0) {
|
||||
monitor.decreaseBrightness();
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: PanelService.getPanel("brightnessPanel", screen)?.toggle(this)
|
||||
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
|
||||
readonly property var now: Time.now
|
||||
|
||||
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
||||
readonly property string clockColor: widgetSettings.clockColor !== undefined ? widgetSettings.clockColor : widgetMetadata.clockColor
|
||||
readonly property bool useCustomFont: widgetSettings.useCustomFont !== undefined ? widgetSettings.useCustomFont : widgetMetadata.useCustomFont
|
||||
readonly property string customFont: widgetSettings.customFont !== undefined ? widgetSettings.customFont : widgetMetadata.customFont
|
||||
readonly property string formatHorizontal: widgetSettings.formatHorizontal !== undefined ? widgetSettings.formatHorizontal : widgetMetadata.formatHorizontal
|
||||
readonly property string formatVertical: widgetSettings.formatVertical !== undefined ? widgetSettings.formatVertical : widgetMetadata.formatVertical
|
||||
readonly property string tooltipFormat: widgetSettings.tooltipFormat !== undefined ? widgetSettings.tooltipFormat : widgetMetadata.tooltipFormat
|
||||
|
||||
readonly property color textColor: Color.resolveColorKey(clockColor)
|
||||
|
||||
// Content dimensions for implicit sizing
|
||||
readonly property real contentWidth: isBarVertical ? capsuleHeight : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.margin2M)
|
||||
readonly property real contentHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.margin2S) : capsuleHeight
|
||||
|
||||
// Size: use implicit width/height
|
||||
// BarWidgetLoader sets explicit width/height to extend click area
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
|
||||
// Visual clock capsule - stays at content size, centered in parent
|
||||
Rectangle {
|
||||
id: visualClock
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
anchors.centerIn: parent
|
||||
|
||||
radius: Style.radiusL
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
anchors.centerIn: parent
|
||||
|
||||
// Horizontal
|
||||
Loader {
|
||||
id: horizontalLoader
|
||||
active: !isBarVertical
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Settings.data.bar.showCapsule ? -5 : -3
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: I18n.locale.toString(now, formatHorizontal.trim()).split("\\n")
|
||||
NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
|
||||
Binding on pointSize {
|
||||
value: {
|
||||
if (repeater.model.length == 1) {
|
||||
// Single line: Full size
|
||||
return barFontSize;
|
||||
} else if (repeater.model.length == 2) {
|
||||
// Two lines: First line is bigger than the second
|
||||
return (index == 0) ? Math.round(barFontSize * 0.9) : Math.round(barFontSize * 0.75);
|
||||
} else {
|
||||
// More than two lines: Make it small!
|
||||
return Math.round(barFontSize * 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
applyUiScale: false
|
||||
color: textColor
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
features: ({
|
||||
"tnum": 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical
|
||||
Loader {
|
||||
id: verticalLoader
|
||||
active: isBarVertical
|
||||
anchors.centerIn: parent // Now this works without layout conflicts
|
||||
sourceComponent: ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: -2
|
||||
Repeater {
|
||||
model: I18n.locale.toString(now, formatVertical.trim()).split(" ")
|
||||
delegate: NText {
|
||||
visible: text !== ""
|
||||
text: modelData
|
||||
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
color: textColor
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
features: ({
|
||||
"tnum": 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.open-calendar"),
|
||||
"action": "open-calendar",
|
||||
"icon": "calendar"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
// Close the context menu
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "open-calendar") {
|
||||
PanelService.getPanel("clockPanel", screen)?.toggle(root);
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build tooltip text with formatted time/date
|
||||
function buildTooltipText() {
|
||||
if (tooltipFormat && tooltipFormat.trim() !== "") {
|
||||
return I18n.locale.toString(now, tooltipFormat.trim());
|
||||
}
|
||||
// Fallback to default if no format is set
|
||||
return I18n.tr("common.calendar"); // Defaults to "Calendar"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clockMouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onEntered: {
|
||||
if (!PanelService.getPanel("clockPanel", screen)?.isPanelOpen) {
|
||||
TooltipService.show(root, buildTooltipText(), BarService.getTooltipDirection(root.screen?.name));
|
||||
tooltipRefreshTimer.start();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
tooltipRefreshTimer.stop();
|
||||
TooltipService.hide();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
TooltipService.hide();
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
} else {
|
||||
PanelService.getPanel("clockPanel", screen)?.toggle(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: tooltipRefreshTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (clockMouseArea.containsMouse && !PanelService.getPanel("clockPanel", screen)?.isPanelOpen) {
|
||||
TooltipService.updateText(buildTooltipText());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string customIcon: widgetSettings.icon !== undefined ? widgetSettings.icon : widgetMetadata.icon
|
||||
readonly property bool useDistroLogo: widgetSettings.useDistroLogo !== undefined ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
readonly property string customIconPath: widgetSettings.customIconPath !== undefined ? widgetSettings.customIconPath : widgetMetadata.customIconPath
|
||||
readonly property bool enableColorization: widgetSettings.enableColorization !== undefined ? widgetSettings.enableColorization : widgetMetadata.enableColorization
|
||||
readonly property string colorizeSystemIcon: widgetSettings.colorizeSystemIcon !== undefined ? widgetSettings.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
|
||||
|
||||
readonly property color iconColor: {
|
||||
if (!enableColorization)
|
||||
return Color.mOnSurface;
|
||||
return Color.resolveColorKey(colorizeSystemIcon);
|
||||
}
|
||||
|
||||
// If we have a custom path and not using distro logo, use the theme icon.
|
||||
// If using distro logo, don't use theme icon.
|
||||
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
|
||||
tooltipText: {
|
||||
if (!screen || PanelService.getPanel("controlCenterPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
return I18n.tr("tooltips.open-control-center");
|
||||
}
|
||||
}
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: iconColor
|
||||
colorBgHover: Color.mHover
|
||||
colorFgHover: Color.mOnHover
|
||||
colorBorder: Style.capsuleBorderColor
|
||||
colorBorderHover: Style.capsuleBorderColor
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.open-launcher"),
|
||||
"action": "open-launcher",
|
||||
"icon": "search"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.open-settings"),
|
||||
"action": "open-settings",
|
||||
"icon": "adjustments"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "open-launcher") {
|
||||
PanelService.toggleLauncher(screen);
|
||||
} else if (action === "open-settings") {
|
||||
var panel = PanelService.getPanel("settingsPanel", screen);
|
||||
panel.requestedTab = SettingsPanel.Tab.General;
|
||||
panel.toggle();
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
|
||||
if (Settings.data.controlCenter.position === "close_to_bar_button") {
|
||||
// Will open the panel next to the bar button.
|
||||
controlCenterPanel?.toggle(this);
|
||||
} else {
|
||||
controlCenterPanel?.toggle();
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
onMiddleClicked: PanelService.toggleLauncher(screen)
|
||||
|
||||
IconImage {
|
||||
id: customOrDistroLogo
|
||||
anchors.centerIn: parent
|
||||
width: root.buttonSize * 0.8
|
||||
height: width
|
||||
source: {
|
||||
if (useDistroLogo)
|
||||
return HostService.osLogo;
|
||||
if (customIconPath !== "")
|
||||
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath;
|
||||
return "";
|
||||
}
|
||||
visible: source !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
layer.enabled: (enableColorization) && (useDistroLogo || customIconPath !== "")
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: !hovering ? iconColor : Color.mOnHover
|
||||
property real colorizeMode: 2.0
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Control
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
|
||||
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
|
||||
readonly property bool leftClickUpdateText: widgetSettings.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText
|
||||
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
|
||||
readonly property bool rightClickUpdateText: widgetSettings.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText
|
||||
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
|
||||
readonly property bool middleClickUpdateText: widgetSettings.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText
|
||||
readonly property string ipcIdentifier: widgetSettings.ipcIdentifier !== undefined ? widgetSettings.ipcIdentifier : (widgetMetadata.ipcIdentifier || "")
|
||||
readonly property string wheelExec: widgetSettings.wheelExec || widgetMetadata.wheelExec
|
||||
readonly property string wheelUpExec: widgetSettings.wheelUpExec || widgetMetadata.wheelUpExec
|
||||
readonly property string wheelDownExec: widgetSettings.wheelDownExec || widgetMetadata.wheelDownExec
|
||||
readonly property string wheelMode: widgetSettings.wheelMode || widgetMetadata.wheelMode
|
||||
readonly property bool wheelUpdateText: widgetSettings.wheelUpdateText ?? widgetMetadata.wheelUpdateText
|
||||
readonly property bool wheelUpUpdateText: widgetSettings.wheelUpUpdateText ?? widgetMetadata.wheelUpUpdateText
|
||||
readonly property bool wheelDownUpdateText: widgetSettings.wheelDownUpdateText ?? widgetMetadata.wheelDownUpdateText
|
||||
readonly property string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "")
|
||||
readonly property bool textStream: widgetSettings.textStream !== undefined ? widgetSettings.textStream : (widgetMetadata.textStream || false)
|
||||
readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000)
|
||||
readonly property string textCollapse: widgetSettings.textCollapse !== undefined ? widgetSettings.textCollapse : (widgetMetadata.textCollapse || "")
|
||||
readonly property bool parseJson: widgetSettings.parseJson !== undefined ? widgetSettings.parseJson : (widgetMetadata.parseJson || false)
|
||||
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec || (wheelMode === "unified" && wheelExec) || (wheelMode === "separate" && (wheelUpExec || wheelDownExec)))
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : true
|
||||
readonly property bool showExecTooltip: widgetSettings.showExecTooltip !== undefined ? widgetSettings.showExecTooltip : (widgetMetadata.showExecTooltip !== undefined ? widgetMetadata.showExecTooltip : true)
|
||||
readonly property bool showTextTooltip: widgetSettings.showTextTooltip !== undefined ? widgetSettings.showTextTooltip : (widgetMetadata.showTextTooltip !== undefined ? widgetMetadata.showTextTooltip : true)
|
||||
readonly property string generalTooltipText: widgetSettings.generalTooltipText !== undefined ? widgetSettings.generalTooltipText : (widgetMetadata.generalTooltipText || "")
|
||||
readonly property bool _hasCustomTooltip: generalTooltipText.trim() !== ""
|
||||
readonly property string hideMode: widgetSettings.hideMode || "alwaysExpanded"
|
||||
readonly property bool hasOutput: _dynamicText !== ""
|
||||
readonly property bool shouldForceOpen: textStream && (hideMode === "alwaysExpanded" || hideMode === "maxTransparent")
|
||||
|
||||
readonly property bool _useNewHideLogic: textCommand && textCommand.length > 0 && textStream
|
||||
readonly property bool _useTextCommandLogic: textCommand && textCommand.length > 0 && textStream
|
||||
|
||||
readonly property bool _pillVisible: {
|
||||
if (!_useTextCommandLogic) {
|
||||
return true;
|
||||
}
|
||||
if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") {
|
||||
return true;
|
||||
}
|
||||
var hasActualIcon = (_dynamicIcon !== "" || customIcon !== "");
|
||||
return hasOutput || (showIcon && hasActualIcon);
|
||||
}
|
||||
|
||||
readonly property real _pillOpacity: {
|
||||
if (!_useTextCommandLogic) {
|
||||
return 1.0;
|
||||
}
|
||||
if (hideMode === "maxTransparent" && !hasOutput) {
|
||||
return 0.0;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
readonly property bool _pillForceOpen: {
|
||||
if (!_useTextCommandLogic) {
|
||||
return _dynamicText !== "" || (textStream && currentMaxTextLength > 0);
|
||||
}
|
||||
if (currentMaxTextLength <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") {
|
||||
return true;
|
||||
}
|
||||
return hasOutput;
|
||||
}
|
||||
|
||||
readonly property string _pillIcon: {
|
||||
if (!_useTextCommandLogic) {
|
||||
if (textCommand && textCommand.length > 0 && showIcon) {
|
||||
return _dynamicIcon !== "" ? _dynamicIcon : customIcon;
|
||||
} else if (!(textCommand && textCommand.length > 0)) {
|
||||
return _dynamicIcon !== "" ? _dynamicIcon : customIcon;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (!showIcon)
|
||||
return "";
|
||||
var actualIcon = _dynamicIcon !== "" ? _dynamicIcon : customIcon;
|
||||
if (hideMode === "expandWithOutput" && actualIcon === "") {
|
||||
return "question-mark";
|
||||
}
|
||||
return actualIcon;
|
||||
}
|
||||
|
||||
readonly property string _pillText: {
|
||||
if (!_useTextCommandLogic) {
|
||||
return (!isVerticalBar || currentMaxTextLength > 0) ? _dynamicText : "";
|
||||
}
|
||||
if (currentMaxTextLength <= 0) {
|
||||
return "";
|
||||
}
|
||||
if (hasOutput) {
|
||||
return _dynamicText;
|
||||
}
|
||||
if (hideMode === "expandWithOutput") {
|
||||
return "";
|
||||
}
|
||||
return " ".repeat(currentMaxTextLength);
|
||||
}
|
||||
|
||||
readonly property bool enableColorization: widgetSettings.enableColorization || false
|
||||
readonly property string colorizeSystemIcon: {
|
||||
if (widgetSettings.colorizeSystemIcon !== undefined)
|
||||
return widgetSettings.colorizeSystemIcon;
|
||||
return widgetMetadata.colorizeSystemIcon !== undefined ? widgetMetadata.colorizeSystemIcon : "none";
|
||||
}
|
||||
|
||||
readonly property bool isColorizing: enableColorization && colorizeSystemIcon !== "none"
|
||||
|
||||
// Get color value from color name (returns null for invalid names)
|
||||
function _getColorValue(colorName, forHover) {
|
||||
const baseColor = (function () {
|
||||
switch (colorName) {
|
||||
case "primary":
|
||||
return Color.mPrimary;
|
||||
case "secondary":
|
||||
return Color.mSecondary;
|
||||
case "tertiary":
|
||||
return Color.mTertiary;
|
||||
case "error":
|
||||
return Color.mError;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
return baseColor !== null ? (forHover ? Qt.darker(baseColor, 1.2) : baseColor) : null;
|
||||
}
|
||||
|
||||
// Resolve icon color with priority: dynamic > static > default
|
||||
function _resolveIconColor(dynamicColorName, staticColorName, isHover) {
|
||||
if (dynamicColorName && dynamicColorName !== "") {
|
||||
if (dynamicColorName === "none") {
|
||||
return isHover ? Color.mOnHover : Color.mOnSurface;
|
||||
}
|
||||
const color = _getColorValue(dynamicColorName, isHover);
|
||||
if (color !== null)
|
||||
return color;
|
||||
}
|
||||
|
||||
if (staticColorName && staticColorName !== "" && isColorizing) {
|
||||
const color = _getColorValue(staticColorName, isHover);
|
||||
if (color !== null)
|
||||
return color;
|
||||
}
|
||||
|
||||
return isHover ? Color.mOnHover : Color.mOnSurface;
|
||||
}
|
||||
|
||||
readonly property color iconColor: _resolveIconColor(_dynamicColor, colorizeSystemIcon, false)
|
||||
readonly property color iconHoverColor: _resolveIconColor(_dynamicColor, colorizeSystemIcon, true)
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
visible: _pillVisible
|
||||
opacity: _pillOpacity
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
icon: _pillIcon
|
||||
text: _pillText
|
||||
rotateText: isVerticalBar && currentMaxTextLength > 0
|
||||
autoHide: false
|
||||
forceOpen: _pillForceOpen
|
||||
forceClose: !_pillForceOpen
|
||||
customTextIconColor: iconColor
|
||||
|
||||
// Helper function to build tooltip content
|
||||
function _buildTooltipContent() {
|
||||
var lines = [];
|
||||
|
||||
// Add custom tooltip if set
|
||||
if (_hasCustomTooltip) {
|
||||
lines.push(generalTooltipText);
|
||||
}
|
||||
|
||||
// Add command details if enabled and available
|
||||
if (showExecTooltip && hasExec) {
|
||||
if (leftClickExec !== "") {
|
||||
lines.push(I18n.tr("bar.custom-button.left-click-label") + `: ${leftClickExec}`);
|
||||
} else if (!leftClickUpdateText) {
|
||||
lines.push(I18n.tr("bar.custom-button.left-click-label") + ": " + I18n.tr("actions.widget-settings"));
|
||||
}
|
||||
|
||||
if (rightClickExec !== "") {
|
||||
lines.push(I18n.tr("bar.custom-button.right-click-label") + `: ${rightClickExec}`);
|
||||
} else if (!rightClickUpdateText) {
|
||||
lines.push(I18n.tr("bar.custom-button.right-click-label") + ": " + I18n.tr("actions.widget-settings"));
|
||||
}
|
||||
|
||||
if (middleClickExec !== "") {
|
||||
lines.push(I18n.tr("bar.custom-button.middle-click-label") + `: ${middleClickExec}`);
|
||||
} else if (!middleClickUpdateText) {
|
||||
lines.push(I18n.tr("bar.custom-button.middle-click-label") + ": " + I18n.tr("actions.widget-settings"));
|
||||
}
|
||||
|
||||
if (wheelMode === "unified" && wheelExec !== "") {
|
||||
lines.push(I18n.tr("bar.custom-button.wheel-label") + `: ${wheelExec}`);
|
||||
} else if (wheelMode === "separate") {
|
||||
if (wheelUpExec !== "") {
|
||||
lines.push(I18n.tr("bar.custom-button.wheel-up") + `: ${wheelUpExec}`);
|
||||
}
|
||||
if (wheelDownExec !== "") {
|
||||
lines.push(I18n.tr("bar.custom-button.wheel-down") + `Wheel down: ${wheelDownExec}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add dynamic text tooltip if enabled and available
|
||||
if (showTextTooltip && _dynamicTooltip !== "") {
|
||||
lines.push(_dynamicTooltip);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
tooltipText: {
|
||||
var lines = _buildTooltipContent();
|
||||
|
||||
// If no custom tooltip and both switches are off, show default tooltip
|
||||
if (!_hasCustomTooltip && !showExecTooltip && !showTextTooltip) {
|
||||
return I18n.tr("bar.custom-button.default-tooltip");
|
||||
}
|
||||
|
||||
// If there's content, join with newlines
|
||||
if (lines.length > 0) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// Fallback (shouldn't reach here normally)
|
||||
return I18n.tr("bar.custom-button.default-tooltip");
|
||||
}
|
||||
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheeled(delta)
|
||||
}
|
||||
|
||||
// Internal state for dynamic text
|
||||
property string _dynamicText: ""
|
||||
property string _dynamicIcon: ""
|
||||
property string _dynamicTooltip: ""
|
||||
property string _dynamicColor: ""
|
||||
|
||||
// Maximum length for text display before scrolling (different values for horizontal and vertical)
|
||||
readonly property var maxTextLength: {
|
||||
"horizontal": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.horizontal !== undefined) ? widgetSettings.maxTextLength.horizontal : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.horizontal !== undefined) ? widgetMetadata.maxTextLength.horizontal : 10)),
|
||||
"vertical": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.vertical !== undefined) ? widgetSettings.maxTextLength.vertical : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.vertical !== undefined) ? widgetMetadata.maxTextLength.vertical : 10))
|
||||
}
|
||||
readonly property int _staticDuration: 6 // How many cycles to stay static at start/end
|
||||
|
||||
// Encapsulated state for scrolling text implementation
|
||||
property var _scrollState: {
|
||||
"originalText": "",
|
||||
"needsScrolling": false,
|
||||
"offset": 0,
|
||||
"phase": 0 // 0=static start, 1=scrolling, 2=static end
|
||||
,
|
||||
"phaseCounter": 0
|
||||
}
|
||||
|
||||
// Current max text length based on bar orientation
|
||||
readonly property int currentMaxTextLength: isVerticalBar ? maxTextLength.vertical : maxTextLength.horizontal
|
||||
|
||||
// Periodically run the text command (if set)
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: Math.max(250, textIntervalMs)
|
||||
repeat: true
|
||||
running: (!isVerticalBar || currentMaxTextLength > 0) && !textStream && textCommand && textCommand.length > 0
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.runTextCommand()
|
||||
}
|
||||
|
||||
// Restart exited text stream commands after a delay
|
||||
Timer {
|
||||
id: restartTimer
|
||||
interval: 1000
|
||||
running: (!isVerticalBar || currentMaxTextLength > 0) && textStream && !textProc.running
|
||||
onTriggered: root.runTextCommand()
|
||||
}
|
||||
|
||||
// Timer for scrolling text display
|
||||
Timer {
|
||||
id: scrollTimer
|
||||
interval: 300
|
||||
repeat: true
|
||||
running: false
|
||||
onTriggered: {
|
||||
if (_scrollState.needsScrolling && _scrollState.originalText.length > currentMaxTextLength) {
|
||||
// Traditional marquee with pause at beginning and end
|
||||
if (_scrollState.phase === 0) {
|
||||
// Static at beginning
|
||||
_dynamicText = _scrollState.originalText.substring(0, Math.min(currentMaxTextLength, _scrollState.originalText.length));
|
||||
_scrollState.phaseCounter++;
|
||||
if (_scrollState.phaseCounter >= _staticDuration) {
|
||||
_scrollState.phaseCounter = 0;
|
||||
_scrollState.phase = 1; // Move to scrolling
|
||||
}
|
||||
} else if (_scrollState.phase === 1) {
|
||||
// Scrolling
|
||||
_scrollState.offset++;
|
||||
var start = _scrollState.offset;
|
||||
var end = start + currentMaxTextLength;
|
||||
|
||||
if (start >= _scrollState.originalText.length - currentMaxTextLength) {
|
||||
// Reached or passed the end, ensure we show the last part
|
||||
var textEnd = _scrollState.originalText.length;
|
||||
var textStart = Math.max(0, textEnd - currentMaxTextLength);
|
||||
_dynamicText = _scrollState.originalText.substring(textStart, textEnd);
|
||||
_scrollState.phase = 2; // Move to static end phase
|
||||
_scrollState.phaseCounter = 0;
|
||||
} else {
|
||||
_dynamicText = _scrollState.originalText.substring(start, end);
|
||||
}
|
||||
} else if (_scrollState.phase === 2) {
|
||||
// Static at end
|
||||
// Ensure end text is displayed correctly
|
||||
var textEnd = _scrollState.originalText.length;
|
||||
var textStart = Math.max(0, textEnd - currentMaxTextLength);
|
||||
_dynamicText = _scrollState.originalText.substring(textStart, textEnd);
|
||||
_scrollState.phaseCounter++;
|
||||
if (_scrollState.phaseCounter >= _staticDuration) {
|
||||
// Do NOT loop back to start, just stop scrolling
|
||||
scrollTimer.stop();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scrollTimer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SplitParser {
|
||||
id: textStdoutSplit
|
||||
onRead: line => root.parseDynamicContent(line)
|
||||
}
|
||||
|
||||
StdioCollector {
|
||||
id: textStdoutCollect
|
||||
onStreamFinished: () => root.parseDynamicContent(this.text)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: textProc
|
||||
stdout: textStream ? textStdoutSplit : textStdoutCollect
|
||||
stderr: StdioCollector {}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (textStream) {
|
||||
Logger.w("CustomButton", `Streaming text command exited (code: ${exitCode}), restarting...`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseDynamicContent(content) {
|
||||
var contentStr = String(content || "").trim();
|
||||
|
||||
if (parseJson && contentStr) {
|
||||
// Handle multi-line JSON by filtering empty lines and joining
|
||||
var jsonStr = contentStr;
|
||||
if (contentStr.includes('\n')) {
|
||||
const lines = contentStr.split('\n').filter(line => line.trim() !== '');
|
||||
jsonStr = lines.join('');
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
const text = parsed.text || "";
|
||||
const icon = parsed.icon || "";
|
||||
let tooltip = parsed.tooltip || "";
|
||||
const color = parsed.color || "";
|
||||
|
||||
// Validate color value
|
||||
const validColors = ["primary", "secondary", "tertiary", "error", "none"];
|
||||
const validColor = (color && validColors.includes(color)) ? color : "";
|
||||
|
||||
if (checkCollapse(text)) {
|
||||
_scrollState.originalText = "";
|
||||
_dynamicText = "";
|
||||
_dynamicIcon = "";
|
||||
_dynamicTooltip = "";
|
||||
_dynamicColor = "";
|
||||
_scrollState.needsScrolling = false;
|
||||
_scrollState.phase = 0;
|
||||
_scrollState.phaseCounter = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollState.originalText = text;
|
||||
_scrollState.needsScrolling = text.length > currentMaxTextLength && currentMaxTextLength > 0;
|
||||
if (_scrollState.needsScrolling) {
|
||||
_dynamicText = text.substring(0, currentMaxTextLength);
|
||||
_scrollState.phase = 0;
|
||||
_scrollState.phaseCounter = 0;
|
||||
_scrollState.offset = 0;
|
||||
scrollTimer.start();
|
||||
} else {
|
||||
_dynamicText = text;
|
||||
scrollTimer.stop();
|
||||
}
|
||||
_dynamicIcon = icon;
|
||||
_dynamicColor = validColor;
|
||||
|
||||
_dynamicTooltip = toHtml(tooltip);
|
||||
_scrollState.offset = 0;
|
||||
return;
|
||||
} catch (e) {
|
||||
Logger.w("CustomButton", `Failed to parse JSON. Content: "${contentStr}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkCollapse(contentStr)) {
|
||||
_scrollState.originalText = "";
|
||||
_dynamicText = "";
|
||||
_dynamicIcon = "";
|
||||
_dynamicTooltip = "";
|
||||
_dynamicColor = "";
|
||||
_scrollState.needsScrolling = false;
|
||||
_scrollState.phase = 0;
|
||||
_scrollState.phaseCounter = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollState.originalText = contentStr;
|
||||
_scrollState.needsScrolling = contentStr.length > currentMaxTextLength && currentMaxTextLength > 0;
|
||||
if (_scrollState.needsScrolling) {
|
||||
_dynamicText = contentStr.substring(0, currentMaxTextLength);
|
||||
_scrollState.phase = 0;
|
||||
_scrollState.phaseCounter = 0;
|
||||
_scrollState.offset = 0;
|
||||
scrollTimer.start();
|
||||
} else {
|
||||
_dynamicText = contentStr;
|
||||
scrollTimer.stop();
|
||||
}
|
||||
_dynamicIcon = "";
|
||||
_dynamicColor = "";
|
||||
_dynamicTooltip = toHtml(contentStr);
|
||||
_scrollState.offset = 0;
|
||||
}
|
||||
|
||||
function checkCollapse(text) {
|
||||
if (!textCollapse || textCollapse.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (textCollapse.startsWith("/") && textCollapse.endsWith("/") && textCollapse.length > 1) {
|
||||
// Treat as regex
|
||||
var pattern = textCollapse.substring(1, textCollapse.length - 1);
|
||||
try {
|
||||
var regex = new RegExp(pattern);
|
||||
return regex.test(text);
|
||||
} catch (e) {
|
||||
Logger.w("CustomButton", `Invalid regex for textCollapse: ${textCollapse} - ${e.message}`);
|
||||
return (textCollapse === text); // Fallback to exact match on invalid regex
|
||||
}
|
||||
} else {
|
||||
// Treat as plain string
|
||||
return (textCollapse === text);
|
||||
}
|
||||
}
|
||||
|
||||
function clicked() {
|
||||
if (leftClickExec) {
|
||||
Quickshell.execDetached(["sh", "-lc", leftClickExec]);
|
||||
Logger.i("CustomButton", `Executing command: ${leftClickExec}`);
|
||||
} else if (!leftClickUpdateText) {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
if (!textStream && leftClickUpdateText) {
|
||||
runTextCommand();
|
||||
}
|
||||
}
|
||||
|
||||
function rightClicked() {
|
||||
if (rightClickExec) {
|
||||
Quickshell.execDetached(["sh", "-lc", rightClickExec]);
|
||||
Logger.i("CustomButton", `Executing command: ${rightClickExec}`);
|
||||
} else if (!rightClickUpdateText) {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
if (!textStream && rightClickUpdateText) {
|
||||
runTextCommand();
|
||||
}
|
||||
}
|
||||
|
||||
function middleClicked() {
|
||||
if (middleClickExec) {
|
||||
Quickshell.execDetached(["sh", "-lc", middleClickExec]);
|
||||
Logger.i("CustomButton", `Executing command: ${middleClickExec}`);
|
||||
} else if (!middleClickUpdateText) {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
if (!textStream && middleClickUpdateText) {
|
||||
runTextCommand();
|
||||
}
|
||||
}
|
||||
|
||||
function toHtml(str) {
|
||||
const htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g;
|
||||
const placeholders = [];
|
||||
let i = 0;
|
||||
const protectedStr = str.replace(htmlTagRegex, tag => {
|
||||
placeholders.push(tag);
|
||||
return `___HTML_TAG_${i++}___`;
|
||||
});
|
||||
|
||||
let escaped = protectedStr.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\r\n|\r|\n/g, "<br/>");
|
||||
|
||||
escaped = escaped.replace(/___HTML_TAG_(\d+)___/g, (_, index) => placeholders[Number(index)]);
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
function runTextCommand() {
|
||||
if (!textCommand || textCommand.length === 0)
|
||||
return;
|
||||
if (textProc.running)
|
||||
return;
|
||||
textProc.command = ["sh", "-lc", textCommand];
|
||||
textProc.running = true;
|
||||
}
|
||||
|
||||
function wheeled(delta) {
|
||||
if (wheelMode === "unified" && wheelExec) {
|
||||
let normalizedDelta = delta > 0 ? 1 : -1;
|
||||
|
||||
let command = wheelExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) {
|
||||
if (operation) {
|
||||
try {
|
||||
let operator = operation.charAt(0);
|
||||
let operand = parseInt(operation.substring(1));
|
||||
|
||||
let result;
|
||||
switch (operator) {
|
||||
case '+':
|
||||
result = normalizedDelta + operand;
|
||||
break;
|
||||
case '-':
|
||||
result = normalizedDelta - operand;
|
||||
break;
|
||||
case '*':
|
||||
result = normalizedDelta * operand;
|
||||
break;
|
||||
case '/':
|
||||
result = Math.floor(normalizedDelta / operand);
|
||||
break;
|
||||
default:
|
||||
result = normalizedDelta;
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
} catch (e) {
|
||||
Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`);
|
||||
return normalizedDelta.toString();
|
||||
}
|
||||
} else {
|
||||
return normalizedDelta.toString();
|
||||
}
|
||||
});
|
||||
|
||||
Quickshell.execDetached(["sh", "-lc", command]);
|
||||
Logger.i("CustomButton", `Executing command: ${command}`);
|
||||
} else if (wheelMode === "separate") {
|
||||
if ((delta > 0 && wheelUpExec) || (delta < 0 && wheelDownExec)) {
|
||||
let commandExec = delta > 0 ? wheelUpExec : wheelDownExec;
|
||||
let normalizedDelta = delta > 0 ? 1 : -1;
|
||||
|
||||
let command = commandExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) {
|
||||
if (operation) {
|
||||
try {
|
||||
let operator = operation.charAt(0);
|
||||
let operand = parseInt(operation.substring(1));
|
||||
|
||||
let result;
|
||||
switch (operator) {
|
||||
case '+':
|
||||
result = normalizedDelta + operand;
|
||||
break;
|
||||
case '-':
|
||||
result = normalizedDelta - operand;
|
||||
break;
|
||||
case '*':
|
||||
result = normalizedDelta * operand;
|
||||
break;
|
||||
case '/':
|
||||
result = Math.floor(normalizedDelta / operand);
|
||||
break;
|
||||
default:
|
||||
result = normalizedDelta;
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
} catch (e) {
|
||||
Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`);
|
||||
return normalizedDelta.toString();
|
||||
}
|
||||
} else {
|
||||
return normalizedDelta.toString();
|
||||
}
|
||||
});
|
||||
|
||||
Quickshell.execDetached(["sh", "-lc", command]);
|
||||
Logger.i("CustomButton", `Executing command: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!textStream) {
|
||||
if (wheelMode === "unified" && wheelUpdateText) {
|
||||
runTextCommand();
|
||||
} else if (wheelMode === "separate") {
|
||||
if ((delta > 0 && wheelUpUpdateText) || (delta < 0 && wheelDownUpdateText)) {
|
||||
runTextCommand();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register IPC button in onLoaded (called by BarWidgetLoader after properties are set).
|
||||
// Component.onCompleted is too early — widgetSettings depends on section/index/screen
|
||||
// which are set by BarWidgetLoader.onLoaded after the component is created.
|
||||
function onLoaded() {
|
||||
if (ipcIdentifier && ipcIdentifier.trim() !== "")
|
||||
CustomButtonIPCService.registerButton(root);
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (ipcIdentifier && ipcIdentifier.trim() !== "")
|
||||
CustomButtonIPCService.unregisterButton(root);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
icon: "dark-mode"
|
||||
tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode")
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: Color.resolveColorKey(iconColorKey)
|
||||
onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
|
||||
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.Power
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
text: IdleInhibitorService.timeout == null ? "" : Time.formatVagueHumanReadableDuration(IdleInhibitorService.timeout)
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
|
||||
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.keep-awake") : I18n.tr("tooltips.keep-awake")
|
||||
onClicked: IdleInhibitorService.manualToggle()
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
forceOpen: IdleInhibitorService.timeout !== null
|
||||
forceClose: IdleInhibitorService.timeout == null
|
||||
onWheel: function (delta) {
|
||||
var sign = delta > 0 ? 1 : -1;
|
||||
// the offset makes scrolling down feel symmetrical to scrolling up
|
||||
var timeout = IdleInhibitorService.timeout - (delta < 0 ? 60 : 0);
|
||||
if (timeout == null || timeout < 600) {
|
||||
delta = 60; // <= 10m, increment at 1m interval
|
||||
} else if (timeout >= 600 && timeout < 1800) {
|
||||
delta = 300; // >= 10m, increment at 5m interval
|
||||
} else if (timeout >= 1800 && timeout < 3600) {
|
||||
delta = 600; // >= 30m, increment at 10m interval
|
||||
} else if (timeout >= 3600) {
|
||||
delta = 1800; // > 1h, increment at 30m interval
|
||||
}
|
||||
|
||||
IdleInhibitorService.changeTimeout(delta * sign);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
|
||||
// Use the shared service for keyboard layout
|
||||
property string currentLayout: KeyboardLayoutService.currentLayout
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: root.showIcon ? "keyboard" : ""
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: isBarVertical ? currentLayout.substring(0, 3).toUpperCase() : currentLayout
|
||||
tooltipText: KeyboardLayoutService.fullLayoutName
|
||||
// When icon is disabled, always show the layout text
|
||||
forceOpen: !root.showIcon || root.displayMode === "forceOpen"
|
||||
forceClose: root.showIcon && root.displayMode === "alwaysHide"
|
||||
onClicked: CompositorService.cycleKeyboardLayout()
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string customIcon: widgetSettings.icon || (widgetMetadata ? widgetMetadata.icon : "rocket")
|
||||
readonly property bool useDistroLogo: widgetSettings.useDistroLogo !== undefined ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
|
||||
readonly property string customIconPath: widgetSettings.customIconPath !== undefined ? widgetSettings.customIconPath : widgetMetadata.customIconPath
|
||||
readonly property bool enableColorization: widgetSettings.enableColorization !== undefined ? widgetSettings.enableColorization : widgetMetadata.enableColorization
|
||||
readonly property string colorizeSystemIcon: widgetSettings.colorizeSystemIcon !== undefined ? widgetSettings.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon
|
||||
|
||||
readonly property color iconColor: {
|
||||
if (!enableColorization)
|
||||
return Color.mOnSurface;
|
||||
return Color.resolveColorKey(colorizeSystemIcon);
|
||||
}
|
||||
|
||||
// If we have a custom path or are using distro logo, don't show the theme icon.
|
||||
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
|
||||
tooltipText: I18n.tr("actions.open-launcher")
|
||||
tooltipDirection: BarService.getTooltipDirection(screenName)
|
||||
baseSize: Style.getCapsuleHeightForScreen(screenName)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: iconColor
|
||||
colorBgHover: Color.mHover
|
||||
colorFgHover: Color.mOnHover
|
||||
colorBorder: Style.capsuleBorderColor
|
||||
colorBorderHover: Style.capsuleBorderColor
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.launcher-settings"),
|
||||
"action": "launcher-settings",
|
||||
"icon": "adjustments"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
}
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "launcher-settings") {
|
||||
var panel = PanelService.getPanel("settingsPanel", screen);
|
||||
panel.requestedTab = SettingsPanel.Tab.Launcher;
|
||||
panel.toggle();
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: PanelService.toggleLauncher(screen)
|
||||
onMiddleClicked: PanelService.toggleLauncher(screen)
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: customOrDistroLogo
|
||||
anchors.centerIn: parent
|
||||
width: root.buttonSize * 0.8
|
||||
height: width
|
||||
source: {
|
||||
if (useDistroLogo)
|
||||
return HostService.osLogo;
|
||||
if (customIconPath !== "")
|
||||
return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath;
|
||||
return "";
|
||||
}
|
||||
visible: source !== ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
layer.enabled: (enableColorization) && (useDistroLogo || customIconPath !== "")
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: !hovering ? iconColor : Color.mOnHover
|
||||
property real colorizeMode: 2.0
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
//test
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Settings
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
|
||||
// Content dimensions for implicit sizing
|
||||
readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(layout.implicitWidth + Style.margin2M)
|
||||
readonly property real contentHeight: isVertical ? Math.round(layout.implicitHeight + Style.margin2M) : capsuleHeight
|
||||
|
||||
readonly property bool hideWhenOff: (widgetSettings.hideWhenOff !== undefined) ? widgetSettings.hideWhenOff : (widgetMetadata.hideWhenOff !== undefined ? widgetMetadata.hideWhenOff : false)
|
||||
|
||||
readonly property string capsIcon: widgetSettings.capsLockIcon !== undefined ? widgetSettings.capsLockIcon : widgetMetadata.capsLockIcon
|
||||
readonly property string numIcon: widgetSettings.numLockIcon !== undefined ? widgetSettings.numLockIcon : widgetMetadata.numLockIcon
|
||||
readonly property string scrollIcon: widgetSettings.scrollLockIcon !== undefined ? widgetSettings.scrollLockIcon : widgetMetadata.scrollLockIcon
|
||||
readonly property bool showCaps: (widgetSettings.showCapsLock !== undefined) ? widgetSettings.showCapsLock : widgetMetadata.showCapsLock
|
||||
readonly property bool showNum: (widgetSettings.showNumLock !== undefined) ? widgetSettings.showNumLock : widgetMetadata.showNumLock
|
||||
readonly property bool showScroll: (widgetSettings.showScrollLock !== undefined) ? widgetSettings.showScrollLock : widgetMetadata.showScrollLock
|
||||
|
||||
visible: !root.hideWhenOff || (root.showCaps && LockKeysService.capsLockOn) || (root.showNum && LockKeysService.numLockOn) || (root.showScroll && LockKeysService.scrollLockOn)
|
||||
|
||||
Component.onCompleted: LockKeysService.registerComponent("bar-lockkeys:" + (screen?.name || "unknown"))
|
||||
Component.onDestruction: LockKeysService.unregisterComponent("bar-lockkeys:" + (screen?.name || "unknown"))
|
||||
|
||||
implicitHeight: contentHeight
|
||||
implicitWidth: contentWidth
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visual capsule centered in parent
|
||||
Rectangle {
|
||||
id: visualCapsule
|
||||
anchors.centerIn: parent
|
||||
color: Style.capsuleColor
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
radius: Style.radiusM
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Item {
|
||||
id: layout
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight
|
||||
implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
spacing: 0
|
||||
visible: !root.isVertical
|
||||
|
||||
NIcon {
|
||||
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
|
||||
icon: root.capsIcon
|
||||
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
|
||||
}
|
||||
|
||||
NIcon {
|
||||
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
|
||||
icon: root.numIcon
|
||||
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
|
||||
}
|
||||
|
||||
NIcon {
|
||||
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
|
||||
icon: root.scrollIcon
|
||||
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: colLayout
|
||||
|
||||
spacing: 0
|
||||
visible: root.isVertical
|
||||
|
||||
NIcon {
|
||||
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
|
||||
icon: root.capsIcon
|
||||
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
|
||||
}
|
||||
|
||||
NIcon {
|
||||
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
|
||||
icon: root.numIcon
|
||||
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
|
||||
}
|
||||
|
||||
NIcon {
|
||||
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
|
||||
icon: root.scrollIcon
|
||||
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea at root level for extended click area
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.RightButton
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
import qs.Widgets.AudioSpectrum
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
// Settings
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Bar orientation (per-screen)
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
|
||||
|
||||
// Widget settings
|
||||
readonly property string hideMode: widgetSettings.hideMode !== undefined ? widgetSettings.hideMode : widgetMetadata.hideMode
|
||||
readonly property bool hideWhenIdle: widgetSettings.hideWhenIdle !== undefined ? widgetSettings.hideWhenIdle : widgetMetadata.hideWhenIdle
|
||||
readonly property bool showAlbumArt: widgetSettings.showAlbumArt !== undefined ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
|
||||
readonly property bool showArtistFirst: widgetSettings.showArtistFirst !== undefined ? widgetSettings.showArtistFirst : widgetMetadata.showArtistFirst
|
||||
readonly property bool showVisualizer: widgetSettings.showVisualizer !== undefined ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
|
||||
readonly property string visualizerType: widgetSettings.visualizerType !== undefined ? widgetSettings.visualizerType : widgetMetadata.visualizerType
|
||||
readonly property string scrollingMode: widgetSettings.scrollingMode !== undefined ? widgetSettings.scrollingMode : widgetMetadata.scrollingMode
|
||||
readonly property bool showProgressRing: widgetSettings.showProgressRing !== undefined ? widgetSettings.showProgressRing : widgetMetadata.showProgressRing
|
||||
readonly property bool useFixedWidth: widgetSettings.useFixedWidth !== undefined ? widgetSettings.useFixedWidth : widgetMetadata.useFixedWidth
|
||||
readonly property real maxWidth: widgetSettings.maxWidth !== undefined ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth, screen ? screen.width * 0.06 : 0)
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
readonly property color textColor: Color.resolveColorKey(textColorKey)
|
||||
|
||||
// Dimensions
|
||||
readonly property int artSize: Style.toOdd(capsuleHeight * 0.75)
|
||||
readonly property int iconSize: Style.toOdd(capsuleHeight * 0.75)
|
||||
readonly property int verticalSize: Style.toOdd(capsuleHeight * 0.85)
|
||||
readonly property int progressWidth: 2
|
||||
|
||||
// State
|
||||
readonly property bool hasPlayer: MediaService.currentPlayer !== null
|
||||
readonly property bool shouldHideIdle: (hideMode === "idle" || hideWhenIdle) && !MediaService.isPlaying
|
||||
readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden"
|
||||
readonly property bool isHidden: shouldHideIdle || shouldHideEmpty
|
||||
|
||||
// Title
|
||||
readonly property string title: {
|
||||
if (!hasPlayer)
|
||||
return I18n.tr("bar.media-mini.no-active-player");
|
||||
var artist = MediaService.trackArtist;
|
||||
var track = MediaService.trackTitle;
|
||||
return showArtistFirst ? (artist ? `${artist} - ${track}` : track) : (artist ? `${track} - ${artist}` : track);
|
||||
}
|
||||
|
||||
// SpectrumService registration for visualizer
|
||||
readonly property string spectrumComponentId: "bar:mediamini:" + root.screen?.name + ":" + root.section + ":" + root.sectionWidgetIndex
|
||||
readonly property bool needsSpectrum: root.showVisualizer && root.visualizerType !== "" && root.visualizerType !== "none" && !root.isHidden
|
||||
|
||||
Layout.preferredHeight: isVertical ? -1 : Style.getBarHeightForScreen(screenName)
|
||||
Layout.preferredWidth: isVertical ? Style.getBarHeightForScreen(screenName) : -1
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: false
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Layout
|
||||
// For horizontal bars, height is always capsuleHeight (no animation needed to prevent jitter)
|
||||
// For vertical bars, collapse to 0 when hidden
|
||||
implicitWidth: isVertical ? (isHidden ? 0 : verticalSize) : (isHidden ? 0 : contentWidth)
|
||||
implicitHeight: isVertical ? (isHidden ? 0 : verticalSize) : capsuleHeight
|
||||
visible: !shouldHideIdle && (hideMode !== "hidden" || opacity > 0)
|
||||
opacity: isHidden ? 0.0 : ((hideMode === "transparent" && !hasPlayer) ? 0.0 : 1.0)
|
||||
|
||||
property real mainContentWidth: 0
|
||||
readonly property real contentWidth: {
|
||||
if (useFixedWidth)
|
||||
return maxWidth;
|
||||
|
||||
// Calculate icon/art width (must match RowLayout visibility)
|
||||
var iconWidth = 0;
|
||||
if (!hasPlayer) {
|
||||
iconWidth = iconSize;
|
||||
} else if (showAlbumArt || showProgressRing) {
|
||||
iconWidth = artSize;
|
||||
}
|
||||
|
||||
var margins = isVertical ? 0 : Style.margin2S;
|
||||
|
||||
// Add spacing and text width
|
||||
var textWidth = 0;
|
||||
if (titleContainer.measuredWidth > 0) {
|
||||
if (iconWidth > 0)
|
||||
margins += Style.marginS;
|
||||
textWidth = titleContainer.measuredWidth + Style.margin2XXS;
|
||||
}
|
||||
|
||||
var total = iconWidth + textWidth + margins;
|
||||
|
||||
// calculate the width of all elements except the scrolling text
|
||||
mainContentWidth = total - textWidth;
|
||||
|
||||
return hasPlayer ? Math.min(total, maxWidth) : total;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
model: {
|
||||
var items = [];
|
||||
if (hasPlayer && MediaService.canPlay) {
|
||||
items.push({
|
||||
"label": MediaService.isPlaying ? I18n.tr("common.pause") : I18n.tr("common.play"),
|
||||
"action": "play-pause",
|
||||
"icon": MediaService.isPlaying ? "media-pause" : "media-play"
|
||||
});
|
||||
}
|
||||
if (hasPlayer && MediaService.canGoPrevious) {
|
||||
items.push({
|
||||
"label": I18n.tr("common.previous"),
|
||||
"action": "previous",
|
||||
"icon": "media-prev"
|
||||
});
|
||||
}
|
||||
if (hasPlayer && MediaService.canGoNext) {
|
||||
items.push({
|
||||
"label": I18n.tr("common.next"),
|
||||
"action": "next",
|
||||
"icon": "media-next"
|
||||
});
|
||||
}
|
||||
|
||||
// Append available players (like in Control Center) so user can switch from the bar
|
||||
var players = MediaService.getAvailablePlayers ? MediaService.getAvailablePlayers() : [];
|
||||
if (players && players.length > 1) {
|
||||
for (var i = 0; i < players.length; i++) {
|
||||
var isCurrent = (i === MediaService.selectedPlayerIndex);
|
||||
items.push({
|
||||
"label": players[i].identity,
|
||||
"action": "player-" + i,
|
||||
"icon": isCurrent ? "check" : "disc",
|
||||
"enabled": true,
|
||||
"visible": true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "play-pause")
|
||||
MediaService.playPause();
|
||||
else if (action === "previous")
|
||||
MediaService.previous();
|
||||
else if (action === "next")
|
||||
MediaService.next();
|
||||
else if (action && action.indexOf("player-") === 0) {
|
||||
var idx = parseInt(action.split("-")[1]);
|
||||
if (!isNaN(idx)) {
|
||||
MediaService.switchToPlayer(idx);
|
||||
}
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main container - stays at content size, pixel-perfect centered in parent
|
||||
Rectangle {
|
||||
id: container
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
width: Style.toOdd(isVertical ? (isHidden ? 0 : verticalSize) : (isHidden ? 0 : contentWidth))
|
||||
height: Style.toOdd(isVertical ? (isHidden ? 0 : verticalSize) : capsuleHeight)
|
||||
radius: Style.radiusM
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: isVertical ? 0 : Style.marginS
|
||||
anchors.rightMargin: isVertical ? 0 : Style.marginS
|
||||
|
||||
// Visualizer
|
||||
Loader {
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
width: Style.toOdd(parent.width)
|
||||
height: Style.toOdd(parent.height)
|
||||
active: showVisualizer
|
||||
z: 0
|
||||
sourceComponent: {
|
||||
if (!showVisualizer)
|
||||
return null;
|
||||
if (visualizerType === "linear")
|
||||
return linearSpectrum;
|
||||
if (visualizerType === "mirrored")
|
||||
return mirroredSpectrum;
|
||||
if (visualizerType === "wave")
|
||||
return waveSpectrum;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal layout
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.marginS
|
||||
visible: !isVertical
|
||||
z: 1
|
||||
|
||||
// Album art / Progress ring
|
||||
Item {
|
||||
visible: hasPlayer && (showAlbumArt || showProgressRing)
|
||||
Layout.preferredWidth: visible ? artSize : 0
|
||||
Layout.preferredHeight: visible ? artSize : 0
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
ProgressRing {
|
||||
id: progressRing
|
||||
anchors.fill: parent
|
||||
visible: showProgressRing
|
||||
progress: MediaService.trackLength > 0 ? MediaService.currentPosition / MediaService.trackLength : 0
|
||||
lineWidth: root.progressWidth
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
visible: showAlbumArt && hasPlayer
|
||||
anchors.fill: parent
|
||||
anchors.margins: showProgressRing ? root.progressWidth * 2 : 0
|
||||
radius: width / 2
|
||||
imagePath: MediaService.trackArtUrl
|
||||
borderWidth: 0
|
||||
imageFillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolling title
|
||||
NScrollText {
|
||||
id: titleContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: capsuleHeight
|
||||
fadeRoundLeftCorners: !(showAlbumArt || showProgressRing)
|
||||
|
||||
text: title
|
||||
|
||||
scrollMode: {
|
||||
if (scrollingMode === "always")
|
||||
return NScrollText.ScrollMode.Always;
|
||||
if (scrollingMode === "hover")
|
||||
return NScrollText.ScrollMode.Hover;
|
||||
return NScrollText.ScrollMode.Never;
|
||||
}
|
||||
cursorShape: hasPlayer ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
maxWidth: root.maxWidth - root.mainContentWidth
|
||||
forcedHover: mainMouseArea.containsMouse
|
||||
fadeExtent: 0.1
|
||||
fadeCornerRadius: Style.radiusM
|
||||
|
||||
NText {
|
||||
color: hasPlayer ? root.textColor : Color.mOnSurfaceVariant
|
||||
pointSize: barFontSize
|
||||
elide: Text.ElideNone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical layout
|
||||
Item {
|
||||
id: verticalLayout
|
||||
visible: isVertical
|
||||
width: Style.toOdd(verticalSize)
|
||||
height: Style.toOdd(width)
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
z: 1
|
||||
|
||||
ProgressRing {
|
||||
anchors.fill: parent
|
||||
visible: showProgressRing
|
||||
progress: MediaService.trackLength > 0 ? MediaService.currentPosition / MediaService.trackLength : 0
|
||||
lineWidth: root.progressWidth
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
visible: showAlbumArt && hasPlayer
|
||||
anchors.fill: parent
|
||||
anchors.margins: showProgressRing ? root.progressWidth * 2 : 0
|
||||
radius: width / 2
|
||||
imagePath: MediaService.trackArtUrl
|
||||
borderWidth: 0
|
||||
imageFillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse interaction moved to root
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse interaction
|
||||
MouseArea {
|
||||
id: mainMouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
// Extend click area to screen edge if widget is at the start/end
|
||||
anchors.leftMargin: (!isVertical && section === "left" && sectionWidgetIndex === 0) ? -Style.marginS : 0
|
||||
anchors.rightMargin: (!isVertical && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginS : 0
|
||||
anchors.topMargin: (isVertical && section === "left" && sectionWidgetIndex === 0) ? -Style.marginM : 0
|
||||
anchors.bottomMargin: (isVertical && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginM : 0
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | Qt.ForwardButton | Qt.BackButton
|
||||
|
||||
onClicked: mouse => {
|
||||
TooltipService.hide();
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
PanelService.getPanel("mediaPlayerPanel", screen)?.toggle(container);
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
PanelService.showContextMenu(contextMenu, container, screen);
|
||||
} else if (mouse.button === Qt.MiddleButton && hasPlayer) {
|
||||
MediaService.playPause();
|
||||
} else if (mouse.button === Qt.ForwardButton && hasPlayer) {
|
||||
MediaService.next();
|
||||
} else if (mouse.button === Qt.BackButton && hasPlayer) {
|
||||
MediaService.previous();
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (!root || !screen) {
|
||||
return;
|
||||
}
|
||||
var scrollMode = scrollingMode;
|
||||
if ((isVertical || scrollMode === "never")) {
|
||||
var panel = PanelService.getPanel("mediaPlayerPanel", screen);
|
||||
if (panel && !panel.isPanelOpen) {
|
||||
TooltipService.show(root, title, BarService.getTooltipDirection(root.screen?.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
|
||||
// Components
|
||||
Component {
|
||||
id: linearSpectrum
|
||||
NLinearSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: 20
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
barPosition: root.barPosition
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mirroredSpectrum
|
||||
NMirroredSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: parent.height - Style.marginS
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: waveSpectrum
|
||||
NWaveSpectrum {
|
||||
width: parent.width - Style.marginS
|
||||
height: parent.height - Style.marginS
|
||||
values: SpectrumService.values
|
||||
fillColor: Color.mPrimary
|
||||
opacity: 0.4
|
||||
mirrored: Settings.data.audio.spectrumMirrored
|
||||
}
|
||||
}
|
||||
|
||||
// Progress Ring Component
|
||||
component ProgressRing: Canvas {
|
||||
property real progress: 0
|
||||
property real lineWidth: 2
|
||||
|
||||
function repaint() {
|
||||
if (this.visible && this.opacity > 0)
|
||||
requestPaint();
|
||||
}
|
||||
|
||||
onProgressChanged: repaint()
|
||||
Component.onCompleted: repaint()
|
||||
|
||||
Connections {
|
||||
target: Color
|
||||
function onMPrimaryChanged() {
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
if (width <= 0 || height <= 0)
|
||||
return;
|
||||
|
||||
var ctx = getContext("2d");
|
||||
var centerX = width / 2;
|
||||
var centerY = height / 2;
|
||||
var radius = Math.min(width, height) / 2 - lineWidth;
|
||||
|
||||
ctx.reset();
|
||||
|
||||
// Background
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = Qt.alpha(Color.mOnSurface, 0.4);
|
||||
ctx.stroke();
|
||||
|
||||
// Progress
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = Color.mPrimary;
|
||||
ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property string middleClickCommand: (widgetSettings.middleClickCommand !== undefined) ? widgetSettings.middleClickCommand : widgetMetadata.middleClickCommand
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstInputVolumeReceived: false
|
||||
property int wheelAccumulator: 0
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
// Connection used to open the pill when input volume changes
|
||||
Connections {
|
||||
target: AudioService.source?.audio ? AudioService.source?.audio : null
|
||||
function onVolumeChanged() {
|
||||
// Logger.i("Bar:Microphone", "onInputVolumeChanged")
|
||||
if (!firstInputVolumeReceived) {
|
||||
// Ignore the first volume change
|
||||
firstInputVolumeReceived = true;
|
||||
} else {
|
||||
// If a tooltip is visible while we show the pill
|
||||
// hide it so it doesn't overlap the volume slider.
|
||||
TooltipService.hide();
|
||||
pill.show();
|
||||
externalHideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection used to open the pill when input mute state changes
|
||||
Connections {
|
||||
target: AudioService.source?.audio ? AudioService.source?.audio : null
|
||||
function onMutedChanged() {
|
||||
// Logger.i("Bar:Microphone", "onInputMutedChanged")
|
||||
if (!firstInputVolumeReceived) {
|
||||
// Ignore the first mute change
|
||||
firstInputVolumeReceived = true;
|
||||
} else {
|
||||
TooltipService.hide();
|
||||
pill.show();
|
||||
externalHideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: externalHideTimer
|
||||
running: false
|
||||
interval: 1500
|
||||
onTriggered: {
|
||||
pill.hide();
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.toggle-mute"),
|
||||
"action": "toggle-mute",
|
||||
"icon": AudioService.inputMuted ? "microphone-off" : "microphone"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.run-custom-command"),
|
||||
"action": "custom-command",
|
||||
"icon": "adjustments"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "toggle-mute") {
|
||||
AudioService.setInputMuted(!AudioService.inputMuted);
|
||||
} else if (action === "custom-command") {
|
||||
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: AudioService.getInputIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: {
|
||||
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
|
||||
const displayVolume = Math.min(maxVolume, AudioService.inputVolume);
|
||||
return Math.round(displayVolume * 100);
|
||||
}
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("audioPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
const nick = AudioService.source?.nickname ?? "";
|
||||
const volumeText = I18n.tr("tooltips.microphone-volume-at", {
|
||||
"volume": (() => {
|
||||
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
|
||||
const displayVolume = Math.min(maxVolume, AudioService.inputVolume);
|
||||
return Math.round(displayVolume * 100);
|
||||
})()
|
||||
});
|
||||
return nick ? volumeText + "\n" + nick : volumeText;
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: function (delta) {
|
||||
// As soon as we start scrolling to adjust volume, hide the tooltip
|
||||
TooltipService.hide();
|
||||
|
||||
wheelAccumulator += delta;
|
||||
if (wheelAccumulator >= 120) {
|
||||
wheelAccumulator = 0;
|
||||
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume);
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
wheelAccumulator = 0;
|
||||
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume);
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
PanelService.getPanel("audioPanel", screen)?.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings // For SettingsPanel
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": NetworkService.wifiEnabled ? I18n.tr("actions.disable-wifi") : I18n.tr("actions.enable-wifi"),
|
||||
"action": "toggle-wifi",
|
||||
"icon": NetworkService.wifiEnabled ? "wifi-off" : "wifi",
|
||||
"enabled": !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("common.wifi") + " " + I18n.tr("tooltips.open-settings"),
|
||||
"action": "wifi-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "toggle-wifi") {
|
||||
NetworkService.setWifiEnabled(!NetworkService.wifiEnabled);
|
||||
} else if (action === "wifi-settings") {
|
||||
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen);
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: NetworkService.getIcon()
|
||||
text: NetworkService.getStatusText(false)
|
||||
autoHide: false
|
||||
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
|
||||
forceClose: isBarVertical || root.displayMode === "alwaysHide" || text === ""
|
||||
onClicked: {
|
||||
var panel = PanelService.getPanel("networkPanel", screen);
|
||||
panel?.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("networkPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
}
|
||||
return NetworkService.getStatusText(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: Settings.data.nightLight.enabled ? Color.mPrimary : Style.capsuleColor
|
||||
colorFg: Settings.data.nightLight.enabled ? Color.mOnPrimary : Color.resolveColorKey(iconColorKey)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
|
||||
tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("common.night-light") : I18n.tr("common.night-light")) : I18n.tr("common.night-light")
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
onClicked: {
|
||||
// Check if wlsunset is available before enabling night light
|
||||
if (!ProgramCheckerService.wlsunsetAvailable) {
|
||||
ToastService.showWarning(I18n.tr("common.night-light"), I18n.tr("toast.night-light.not-installed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Settings.data.nightLight.enabled) {
|
||||
Settings.data.nightLight.enabled = true;
|
||||
Settings.data.nightLight.forced = false;
|
||||
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
|
||||
Settings.data.nightLight.forced = true;
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false;
|
||||
Settings.data.nightLight.forced = false;
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Power
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: PowerProfileService.noctaliaPerformanceMode ? Color.mPrimary : Style.capsuleColor
|
||||
colorFg: PowerProfileService.noctaliaPerformanceMode ? Color.mOnPrimary : Color.resolveColorKey(iconColorKey)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off"
|
||||
tooltipText: PowerProfileService.noctaliaPerformanceMode ? I18n.tr("tooltips.noctalia-performance-enabled") : I18n.tr("tooltips.noctalia-performance-enabled")
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
onClicked: PowerProfileService.toggleNoctaliaPerformance()
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
readonly property bool showUnreadBadge: widgetSettings.showUnreadBadge !== undefined ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
|
||||
readonly property bool hideWhenZero: widgetSettings.hideWhenZero !== undefined ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
|
||||
readonly property bool hideWhenZeroUnread: widgetSettings.hideWhenZeroUnread !== undefined ? widgetSettings.hideWhenZeroUnread : widgetMetadata.hideWhenZeroUnread
|
||||
readonly property string unreadBadgeColor: widgetSettings.unreadBadgeColor !== undefined ? widgetSettings.unreadBadgeColor : widgetMetadata.unreadBadgeColor
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
readonly property color badgeColor: Color.resolveColorKey(unreadBadgeColor)
|
||||
|
||||
function computeUnreadCount() {
|
||||
var since = NotificationService.lastSeenTs;
|
||||
var count = 0;
|
||||
var model = NotificationService.historyModel;
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var item = model.get(i);
|
||||
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp;
|
||||
if (ts > since)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
readonly property int count: computeUnreadCount()
|
||||
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
icon: NotificationService.doNotDisturb ? "bell-off" : "bell"
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("notificationHistoryPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
return I18n.tr("tooltips.open-notification-history-enable-dnd");
|
||||
}
|
||||
}
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: Color.resolveColorKey(iconColorKey)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
visible: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0))
|
||||
opacity: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0)) ? 1.0 : 0.0
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": NotificationService.doNotDisturb ? I18n.tr("actions.disable-dnd") : I18n.tr("actions.enable-dnd"),
|
||||
"action": "toggle-dnd",
|
||||
"icon": NotificationService.doNotDisturb ? "bell" : "bell-off"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.clear-history"),
|
||||
"action": "clear-history",
|
||||
"icon": "trash"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "toggle-dnd") {
|
||||
NotificationService.doNotDisturb = !NotificationService.doNotDisturb;
|
||||
} else if (action === "clear-history") {
|
||||
NotificationService.clearHistory();
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var panel = PanelService.getPanel("notificationHistoryPanel", screen);
|
||||
panel?.toggle(this);
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenterOffset: parent.baseSize / 4
|
||||
anchors.verticalCenterOffset: -parent.baseSize / 4
|
||||
z: 2
|
||||
active: showUnreadBadge
|
||||
sourceComponent: Rectangle {
|
||||
id: badge
|
||||
height: 7
|
||||
width: height
|
||||
radius: Style.radiusXS
|
||||
color: root.hovering ? Color.mOnHover : (root.badgeColor || Color.mError)
|
||||
border.color: Color.mSurface
|
||||
border.width: Style.borderS
|
||||
visible: count > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Commons
|
||||
import qs.Services.Power
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
visible: PowerProfileService.available
|
||||
icon: PowerProfileService.getIcon()
|
||||
tooltipText: I18n.tr("tooltips.power-profile", {
|
||||
"profile": PowerProfileService.getName()
|
||||
})
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? Style.capsuleColor : Color.mPrimary
|
||||
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.resolveColorKey(iconColorKey) : Color.mOnPrimary
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
onClicked: PowerProfileService.cycleProfile()
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string iconColorKey: (widgetSettings.iconColor !== undefined) ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
baseSize: Style.getCapsuleHeightForScreen(screenName)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
icon: "power"
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("sessionMenuPanel", screen)?.isPanelOpen)
|
||||
return "";
|
||||
else
|
||||
return I18n.tr("tooltips.session-menu");
|
||||
}
|
||||
tooltipDirection: BarService.getTooltipDirection(screenName)
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: Color.resolveColorKey(iconColorKey)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: PanelService.getPanel("sessionMenuPanel", screen)?.toggle()
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string valueIconColor: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
readonly property color iconColor: Color.resolveColorKey(valueIconColor)
|
||||
|
||||
icon: "settings"
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("settingsPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
return I18n.tr("tooltips.open-settings");
|
||||
}
|
||||
}
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: iconColor
|
||||
colorBgHover: Color.mHover
|
||||
colorFgHover: Color.mOnHover
|
||||
colorBorder: Style.capsuleBorderColor
|
||||
colorBorderHover: Style.capsuleBorderColor
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (Settings.data.ui.settingsPanelMode === "attached") {
|
||||
PanelService.getPanel("settingsPanel", screen)?.toggle(this);
|
||||
} else {
|
||||
PanelService.getPanel("settingsPanel", screen)?.toggle();
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(screenName)
|
||||
readonly property int spacerSize: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
|
||||
|
||||
implicitWidth: isBarVertical ? barHeight : spacerSize
|
||||
implicitHeight: isBarVertical ? spacerSize : barHeight
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
}
|
||||
@@ -0,0 +1,967 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
|
||||
|
||||
readonly property bool compactMode: widgetSettings.compactMode !== undefined ? widgetSettings.compactMode : widgetMetadata.compactMode
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
|
||||
readonly property bool useMonospaceFont: widgetSettings.useMonospaceFont !== undefined ? widgetSettings.useMonospaceFont : widgetMetadata.useMonospaceFont
|
||||
readonly property bool usePadding: !compactMode && !isVertical && useMonospaceFont && ((widgetSettings.usePadding !== undefined) ? widgetSettings.usePadding : widgetMetadata.usePadding)
|
||||
|
||||
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
|
||||
readonly property bool showCpuCores: (widgetSettings.showCpuCores !== undefined) ? widgetSettings.showCpuCores : widgetMetadata.showCpuCores
|
||||
readonly property bool showCpuFreq: (widgetSettings.showCpuFreq !== undefined) ? widgetSettings.showCpuFreq : widgetMetadata.showCpuFreq
|
||||
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
|
||||
readonly property bool showGpuTemp: (widgetSettings.showGpuTemp !== undefined) ? widgetSettings.showGpuTemp : widgetMetadata.showGpuTemp
|
||||
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
|
||||
readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
|
||||
readonly property bool showSwapUsage: (widgetSettings.showSwapUsage !== undefined) ? widgetSettings.showSwapUsage : widgetMetadata.showSwapUsage
|
||||
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
|
||||
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
|
||||
readonly property bool showDiskUsageAsPercent: (widgetSettings.showDiskUsageAsPercent !== undefined) ? widgetSettings.showDiskUsageAsPercent : widgetMetadata.showDiskUsageAsPercent
|
||||
readonly property bool showDiskAvailable: (widgetSettings.showDiskAvailable !== undefined) ? widgetSettings.showDiskAvailable : widgetMetadata.showDiskAvailable
|
||||
readonly property bool showLoadAverage: (widgetSettings.showLoadAverage !== undefined) ? widgetSettings.showLoadAverage : widgetMetadata.showLoadAverage
|
||||
readonly property string diskPath: (widgetSettings.diskPath !== undefined) ? widgetSettings.diskPath : widgetMetadata.diskPath
|
||||
readonly property string fontFamily: useMonospaceFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault
|
||||
|
||||
readonly property int paddingPercent: usePadding ? String("100%").length : 0
|
||||
readonly property int paddingTemp: usePadding ? String("999°").length : 0
|
||||
readonly property int paddingCpuFreq: usePadding ? String("9.9").length : 0
|
||||
readonly property int paddingSpeed: usePadding ? String("9999G").length : 0
|
||||
|
||||
readonly property real iconSize: Style.toOdd(capsuleHeight * 0.48)
|
||||
readonly property real miniGaugeWidth: Math.max(3, Style.toOdd(root.iconSize * 0.25))
|
||||
|
||||
// Content dimensions for implicit sizing
|
||||
readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(mainGrid.implicitWidth + Style.margin2M)
|
||||
readonly property real contentHeight: isVertical ? Math.round(mainGrid.implicitHeight + Style.margin2M) : capsuleHeight
|
||||
|
||||
readonly property color iconColor: Color.resolveColorKey(iconColorKey)
|
||||
readonly property color textColor: Color.resolveColorKey(textColorKey)
|
||||
|
||||
// Size: use implicit width/height
|
||||
// BarWidgetLoader sets explicit width/height to extend click area
|
||||
implicitWidth: contentWidth
|
||||
implicitHeight: contentHeight
|
||||
|
||||
Component.onCompleted: SystemStatService.registerComponent("bar-sysmon:" + (screen?.name || "unknown"))
|
||||
Component.onDestruction: SystemStatService.unregisterComponent("bar-sysmon:" + (screen?.name || "unknown"))
|
||||
|
||||
function openExternalMonitor() {
|
||||
Quickshell.execDetached(["sh", "-c", Settings.data.systemMonitor.externalMonitor]);
|
||||
}
|
||||
|
||||
// Build comprehensive tooltip text with all stats
|
||||
function buildTooltipContent() {
|
||||
let rows = [];
|
||||
|
||||
// CPU
|
||||
rows.push([I18n.tr("system-monitor.cpu-usage"), `${Math.round(SystemStatService.cpuUsage)}% (${SystemStatService.cpuFreq.replace(/[^0-9.]/g, "")} GHz)`]);
|
||||
if (showCpuCores) {
|
||||
SystemStatService.coresUsage.forEach((usage, core) => rows.push([" " + I18n.tr("system-monitor.core-usage", {
|
||||
"id": core
|
||||
}), `${Math.round(usage)}%`]));
|
||||
}
|
||||
|
||||
if (SystemStatService.cpuTemp > 0) {
|
||||
rows.push([I18n.tr("system-monitor.cpu-temp"), `${Math.round(SystemStatService.cpuTemp)}°C`]);
|
||||
}
|
||||
|
||||
// GPU (if available)
|
||||
if (SystemStatService.gpuAvailable) {
|
||||
rows.push([I18n.tr("system-monitor.gpu-temp"), `${Math.round(SystemStatService.gpuTemp)}°C`]);
|
||||
}
|
||||
|
||||
// Load Average
|
||||
if (SystemStatService.loadAvg1 >= 0) {
|
||||
rows.push([I18n.tr("system-monitor.load-average"), `${SystemStatService.loadAvg1.toFixed(2)} · ${SystemStatService.loadAvg5.toFixed(2)} · ${SystemStatService.loadAvg15.toFixed(2)}`]);
|
||||
}
|
||||
|
||||
// Memory
|
||||
rows.push([I18n.tr("common.memory"), `${Math.round(SystemStatService.memPercent)}% (${(SystemStatService.memGb).toFixed(1)} GiB)`]);
|
||||
|
||||
// Swap (if available)
|
||||
if (SystemStatService.swapTotalGb > 0) {
|
||||
rows.push([I18n.tr("bar.system-monitor.swap-usage-label"), `${Math.round(SystemStatService.swapPercent)}% (${(SystemStatService.swapGb).toFixed(1)} GiB)`]);
|
||||
}
|
||||
|
||||
// Network
|
||||
rows.push([I18n.tr("system-monitor.download-speed"), `${SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]);
|
||||
rows.push([I18n.tr("system-monitor.upload-speed"), `${SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]);
|
||||
|
||||
// Disk
|
||||
const diskPercent = SystemStatService.diskPercents[diskPath];
|
||||
if (diskPercent !== undefined) {
|
||||
const usedGb = SystemStatService.diskUsedGb[diskPath] || 0;
|
||||
const sizeGb = SystemStatService.diskSizeGb[diskPath] || 0;
|
||||
const availGb = SystemStatService.diskAvailableGb[diskPath] || 0;
|
||||
rows.push([I18n.tr("system-monitor.disk"), `${diskPercent}% (${usedGb.toFixed(1)} / ${sizeGb.toFixed(1)} GB)`]);
|
||||
rows.push([I18n.tr("common.available"), `${availGb.toFixed(1)} GB`]);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Visibility-aware warning/critical states (delegates to service)
|
||||
readonly property bool cpuWarning: showCpuUsage && SystemStatService.cpuWarning
|
||||
readonly property bool cpuCritical: showCpuUsage && SystemStatService.cpuCritical
|
||||
readonly property bool tempWarning: showCpuTemp && SystemStatService.tempWarning
|
||||
readonly property bool tempCritical: showCpuTemp && SystemStatService.tempCritical
|
||||
readonly property bool gpuWarning: showGpuTemp && SystemStatService.gpuWarning
|
||||
readonly property bool gpuCritical: showGpuTemp && SystemStatService.gpuCritical
|
||||
readonly property bool memWarning: showMemoryUsage && SystemStatService.memWarning
|
||||
readonly property bool memCritical: showMemoryUsage && SystemStatService.memCritical
|
||||
readonly property bool swapWarning: showSwapUsage && SystemStatService.swapWarning
|
||||
readonly property bool swapCritical: showSwapUsage && SystemStatService.swapCritical
|
||||
readonly property bool diskWarning: showDiskUsage && SystemStatService.isDiskWarning(diskPath)
|
||||
readonly property bool diskCritical: showDiskUsage && SystemStatService.isDiskCritical(diskPath)
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("system-monitor.title"),
|
||||
"action": "sysmon-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "sysmon-settings") {
|
||||
let monitorCmd = Settings.data.systemMonitor.externalMonitor;
|
||||
if (monitorCmd && monitorCmd.trim() !== "") {
|
||||
openExternalMonitor();
|
||||
} else {
|
||||
SettingsPanelService.openToTab(SettingsPanel.Tab.System, 0, screen);
|
||||
}
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visual capsule centered in parent
|
||||
Rectangle {
|
||||
id: visualCapsule
|
||||
width: root.contentWidth
|
||||
height: root.contentHeight
|
||||
anchors.centerIn: parent
|
||||
radius: Style.radiusM
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
// Mini gauge component for compact mode, vertical gauge that fills from bottom
|
||||
Component {
|
||||
id: miniGaugeComponent
|
||||
|
||||
NLinearGauge {
|
||||
ratio: 0
|
||||
orientation: Qt.Vertical
|
||||
fillColor: Color.mPrimary
|
||||
width: miniGaugeWidth
|
||||
height: iconSize
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: mainGrid
|
||||
anchors.centerIn: parent
|
||||
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: isVertical ? -1 : 1
|
||||
columns: isVertical ? 1 : -1
|
||||
rowSpacing: isVertical ? (compactMode ? Style.marginL : Style.marginXL) : 0
|
||||
columnSpacing: isVertical ? 0 : Style.marginM
|
||||
|
||||
// CPU Usage Component
|
||||
Item {
|
||||
id: cpuUsageContainer
|
||||
implicitWidth: cpuUsageContent.implicitWidth
|
||||
implicitHeight: cpuUsageContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showCpuUsage
|
||||
|
||||
GridLayout {
|
||||
id: cpuUsageContent
|
||||
anchors.centerIn: parent
|
||||
|
||||
property bool verticalDisplay: isVertical && (!compactMode || showCpuCores)
|
||||
flow: verticalDisplay ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: verticalDisplay ? -1 : 1
|
||||
columns: verticalDisplay ? 1 : -1
|
||||
rowSpacing: compactMode ? 3 : Style.marginXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: (cpuWarning || cpuCritical) ? SystemStatService.cpuColor : root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: `${Math.round(SystemStatService.cpuUsage)}%`.padStart(paddingPercent, " ")
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: (cpuWarning || cpuCritical) ? SystemStatService.cpuColor : root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode general cpu
|
||||
Loader {
|
||||
active: compactMode && !showCpuCores
|
||||
visible: compactMode && !showCpuCores
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.cpuUsage / 100);
|
||||
item.fillColor = Qt.binding(() => SystemStatService.cpuColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Compact mode for cores
|
||||
Repeater {
|
||||
model: (compactMode && showCpuCores) ? SystemStatService.coresUsage : []
|
||||
|
||||
delegate: NLinearGauge {
|
||||
required property var modelData
|
||||
width: isVertical ? iconSize : miniGaugeWidth
|
||||
height: isVertical ? miniGaugeWidth : iconSize
|
||||
orientation: isVertical ? Qt.Horizontal : Qt.Vertical
|
||||
ratio: modelData / 100
|
||||
fillColor: SystemStatService.getCoreUsageColor(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU Frequency Component
|
||||
Item {
|
||||
id: cpuFreqContainer
|
||||
implicitWidth: cpuFreqContent.implicitWidth
|
||||
implicitHeight: cpuFreqContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showCpuFreq && (!isVertical || compactMode)
|
||||
|
||||
GridLayout {
|
||||
id: cpuFreqContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-usage"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: SystemStatService.cpuFreq.replace("Hz", "").replace(" ", "").padStart(paddingCpuFreq, " ")
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.cpuFreqRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPU Temperature Component
|
||||
Item {
|
||||
id: cpuTempContainer
|
||||
implicitWidth: cpuTempContent.implicitWidth
|
||||
implicitHeight: cpuTempContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showCpuTemp
|
||||
|
||||
GridLayout {
|
||||
id: cpuTempContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "cpu-temperature"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: (tempWarning || tempCritical) ? SystemStatService.tempColor : root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: `${Math.round(SystemStatService.cpuTemp)}°`.padStart(paddingTemp, " ")
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: (tempWarning || tempCritical) ? SystemStatService.tempColor : root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode, mini gauge (to the right of icon)
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.cpuTemp / 100);
|
||||
item.fillColor = Qt.binding(() => SystemStatService.tempColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GPU Temperature Component
|
||||
Item {
|
||||
id: gpuTempContainer
|
||||
implicitWidth: gpuTempContent.implicitWidth
|
||||
implicitHeight: gpuTempContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showGpuTemp && SystemStatService.gpuAvailable
|
||||
|
||||
GridLayout {
|
||||
id: gpuTempContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "gpu-temperature"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: (gpuWarning || gpuCritical) ? SystemStatService.gpuColor : root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: `${Math.round(SystemStatService.gpuTemp)}°`.padStart(paddingTemp, " ")
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: (gpuWarning || gpuCritical) ? SystemStatService.gpuColor : root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.gpuTemp / 100);
|
||||
item.fillColor = Qt.binding(() => SystemStatService.gpuColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load Average Component
|
||||
Item {
|
||||
id: loadAvgContainer
|
||||
implicitWidth: loadAvgContent.implicitWidth
|
||||
implicitHeight: loadAvgContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showLoadAverage && SystemStatService.nproc > 0 && SystemStatService.loadAvg1 > 0
|
||||
|
||||
GridLayout {
|
||||
id: loadAvgContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "weight"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: `${SystemStatService.loadAvg1.toFixed(1)}`.padStart(usePadding ? `${SystemStatService.nproc.toFixed(1)}`.length : 0, " ")
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => Math.min(1, SystemStatService.loadAvg1 / SystemStatService.nproc));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Memory Usage Component
|
||||
Item {
|
||||
id: memoryContainer
|
||||
implicitWidth: memoryContent.implicitWidth
|
||||
implicitHeight: memoryContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showMemoryUsage
|
||||
|
||||
GridLayout {
|
||||
id: memoryContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "memory"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: (memWarning || memCritical) ? SystemStatService.memColor : root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: SystemStatService.formatRamDisplay({
|
||||
percent: showMemoryAsPercent,
|
||||
padding: usePadding
|
||||
})
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: (memWarning || memCritical) ? SystemStatService.memColor : root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.memPercent / 100);
|
||||
item.fillColor = Qt.binding(() => SystemStatService.memColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swap Usage Component
|
||||
Item {
|
||||
id: swapContainer
|
||||
implicitWidth: swapContent.implicitWidth
|
||||
implicitHeight: swapContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showSwapUsage && SystemStatService.swapTotalGb > 0
|
||||
|
||||
GridLayout {
|
||||
id: swapContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "exchange"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: (swapWarning || swapCritical) ? SystemStatService.swapColor : root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: SystemStatService.formatRamDisplay({
|
||||
swap: true,
|
||||
percent: true,
|
||||
padding: usePadding
|
||||
})
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: (swapWarning || swapCritical) ? SystemStatService.swapColor : root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.swapPercent / 100);
|
||||
item.fillColor = Qt.binding(() => SystemStatService.swapColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Download Speed Component
|
||||
Item {
|
||||
implicitWidth: downloadContent.implicitWidth
|
||||
implicitHeight: downloadContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
GridLayout {
|
||||
id: downloadContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "download-speed"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.rxSpeed) : SystemStatService.formatSpeed(SystemStatService.rxSpeed).padStart(paddingSpeed, " ")
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.rxRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network Upload Speed Component
|
||||
Item {
|
||||
implicitWidth: uploadContent.implicitWidth
|
||||
implicitHeight: uploadContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showNetworkStats
|
||||
|
||||
GridLayout {
|
||||
id: uploadContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "upload-speed"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.txSpeed) : SystemStatService.formatSpeed(SystemStatService.txSpeed).padStart(paddingSpeed, " ")
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => SystemStatService.txRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk Usage Component (primary drive)
|
||||
Item {
|
||||
id: diskContainer
|
||||
implicitWidth: diskContent.implicitWidth
|
||||
implicitHeight: diskContent.implicitHeight
|
||||
Layout.preferredWidth: isVertical ? root.width : implicitWidth
|
||||
Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight
|
||||
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
|
||||
visible: showDiskUsage
|
||||
|
||||
GridLayout {
|
||||
id: diskContent
|
||||
anchors.centerIn: parent
|
||||
flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight
|
||||
rows: (isVertical && !compactMode) ? 2 : 1
|
||||
columns: (isVertical && !compactMode) ? 1 : 2
|
||||
rowSpacing: Style.marginXXS
|
||||
columnSpacing: compactMode ? 3 : Style.marginXS
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: (isVertical && !compactMode) ? 1 : 0
|
||||
Layout.column: 0
|
||||
|
||||
NIcon {
|
||||
icon: "storage"
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, contentHeight)
|
||||
color: (diskWarning || diskCritical) ? SystemStatService.getDiskColor(diskPath) : root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text mode
|
||||
NText {
|
||||
visible: !compactMode
|
||||
text: SystemStatService.formatDiskDisplay(diskPath, {
|
||||
percent: showDiskUsageAsPercent,
|
||||
available: showDiskAvailable,
|
||||
padding: usePadding
|
||||
})
|
||||
family: fontFamily
|
||||
pointSize: barFontSize
|
||||
applyUiScale: false
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: (diskWarning || diskCritical) ? SystemStatService.getDiskColor(diskPath) : root.textColor
|
||||
Layout.row: isVertical ? 0 : 0
|
||||
Layout.column: isVertical ? 0 : 1
|
||||
}
|
||||
|
||||
// Compact mode
|
||||
Loader {
|
||||
active: compactMode
|
||||
visible: compactMode
|
||||
sourceComponent: miniGaugeComponent
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.row: 0
|
||||
Layout.column: 1
|
||||
|
||||
onLoaded: {
|
||||
item.ratio = Qt.binding(() => (showDiskAvailable ? SystemStatService.diskAvailPercents[diskPath] : SystemStatService.diskPercents[diskPath] ?? 0) / 100);
|
||||
item.fillColor = Qt.binding(() => SystemStatService.getDiskColor(diskPath, showDiskAvailable));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea at root level for extended click area
|
||||
MouseArea {
|
||||
id: tooltipArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
PanelService.getPanel("systemStatsPanel", screen)?.toggle(root);
|
||||
TooltipService.hide();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
TooltipService.hide();
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
TooltipService.hide();
|
||||
openExternalMonitor();
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
if (!PanelService.getPanel("systemStatsPanel", screen).isPanelOpen) {
|
||||
TooltipService.show(root, buildTooltipContent(), BarService.getTooltipDirection(root.screen?.name));
|
||||
tooltipRefreshTimer.start();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
tooltipRefreshTimer.stop();
|
||||
TooltipService.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: tooltipRefreshTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (tooltipArea.containsMouse) {
|
||||
TooltipService.updateText(buildTooltipContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,580 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Trigger re-evaluation when window is registered
|
||||
property int popupMenuUpdateTrigger: 0
|
||||
|
||||
// Get shared popup menu window from PanelService (reactive to trigger changes)
|
||||
readonly property var popupMenuWindow: {
|
||||
// Reference trigger to force re-evaluation
|
||||
var popupMenuUpdateTriggerRef = popupMenuUpdateTrigger;
|
||||
return PanelService.getPopupMenuWindow(screen);
|
||||
}
|
||||
|
||||
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
|
||||
|
||||
Connections {
|
||||
target: PanelService
|
||||
function onPopupMenuWindowRegistered(registeredScreen) {
|
||||
if (registeredScreen === screen) {
|
||||
root.popupMenuUpdateTrigger++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(screenName)
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property bool density: Settings.data.bar.density
|
||||
readonly property int iconSize: Style.toOdd(capsuleHeight * 0.65)
|
||||
|
||||
property var blacklist: widgetSettings.blacklist || widgetMetadata.blacklist || [] // Read from settings
|
||||
property var pinned: widgetSettings.pinned || widgetMetadata.pinned || [] // Pinned items (shown inline)
|
||||
property bool drawerEnabled: widgetSettings.drawerEnabled !== undefined ? widgetSettings.drawerEnabled : (widgetMetadata.drawerEnabled !== undefined ? widgetMetadata.drawerEnabled : true) // Enable drawer panel
|
||||
property bool hidePassive: widgetSettings.hidePassive !== undefined ? widgetSettings.hidePassive : true // Hide passive status items
|
||||
readonly property string chevronColorKey: widgetSettings.chevronColor !== undefined ? widgetSettings.chevronColor : widgetMetadata.chevronColor
|
||||
readonly property color chevronColor: Color.resolveColorKey(chevronColorKey)
|
||||
property var filteredItems: [] // Items to show inline (pinned)
|
||||
property var dropdownItems: [] // Items to show in drawer (unpinned)
|
||||
property int hoveredItemIndex: -1 // Track hovered item for dot indicator
|
||||
|
||||
Timer {
|
||||
id: updateDebounceTimer
|
||||
interval: 100 // milliseconds
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: _performFilteredItemsUpdate()
|
||||
}
|
||||
|
||||
readonly property var statusSignature: {
|
||||
if (!SystemTray.items || !SystemTray.items.values) {
|
||||
return "";
|
||||
}
|
||||
var sig = "";
|
||||
var items = SystemTray.items.values;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if (item) {
|
||||
// Direct property access creates reactive binding
|
||||
var s = item.status;
|
||||
sig += (item.id || i) + ":" + (s !== undefined ? s : -1);
|
||||
}
|
||||
}
|
||||
// Trigger update when signature changes (status changed)
|
||||
if (root.hidePassive) {
|
||||
Qt.callLater(root.updateFilteredItems);
|
||||
}
|
||||
return sig;
|
||||
}
|
||||
Repeater {
|
||||
id: statusConnectionsRepeater
|
||||
model: SystemTray.items && SystemTray.items.values ? SystemTray.items.values : []
|
||||
|
||||
delegate: Item {
|
||||
Connections {
|
||||
target: modelData
|
||||
enabled: modelData !== null && modelData !== undefined
|
||||
function onStatusChanged() {
|
||||
if (root.hidePassive) {
|
||||
root.updateFilteredItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _performFilteredItemsUpdate() {
|
||||
// Force a fresh read of settings to ensure we have the latest blacklist
|
||||
var currentSettings = {};
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var w = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (w && sectionWidgetIndex < w.length) {
|
||||
currentSettings = w[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// Update local properties with fresh data
|
||||
if (currentSettings.blacklist !== undefined)
|
||||
root.blacklist = currentSettings.blacklist;
|
||||
if (currentSettings.pinned !== undefined)
|
||||
root.pinned = currentSettings.pinned;
|
||||
|
||||
let newItems = [];
|
||||
if (SystemTray.items && SystemTray.items.values) {
|
||||
const trayItems = SystemTray.items.values;
|
||||
for (var i = 0; i < trayItems.length; i++) {
|
||||
const item = trayItems[i];
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = item.tooltipTitle || item.name || item.id || "";
|
||||
|
||||
// Skip passive items if hidePassive is enabled
|
||||
if (root.hidePassive && item.status !== undefined && (item.status === SystemTray.Passive || item.status === 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if blacklisted
|
||||
let isBlacklisted = false;
|
||||
if (root.blacklist && root.blacklist.length > 0) {
|
||||
for (var j = 0; j < root.blacklist.length; j++) {
|
||||
const rule = root.blacklist[j];
|
||||
if (wildCardMatch(title, rule)) {
|
||||
isBlacklisted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlacklisted) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If drawer is disabled, show all items inline
|
||||
if (!root.drawerEnabled) {
|
||||
filteredItems = newItems;
|
||||
dropdownItems = [];
|
||||
} else {
|
||||
// Build inline (pinned) and drawer (unpinned) lists
|
||||
// If pinned list is empty, all items go to drawer (none inline)
|
||||
// If pinned list has items, pinned items are inline, rest go to drawer
|
||||
if (pinned && pinned.length > 0) {
|
||||
let pinnedItems = [];
|
||||
for (var k = 0; k < newItems.length; k++) {
|
||||
const item2 = newItems[k];
|
||||
const title2 = item2.tooltipTitle || item2.name || item2.id || "";
|
||||
for (var m = 0; m < pinned.length; m++) {
|
||||
const rule2 = pinned[m];
|
||||
if (wildCardMatch(title2, rule2)) {
|
||||
pinnedItems.push(item2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
filteredItems = pinnedItems;
|
||||
|
||||
// Unpinned items go to drawer
|
||||
let unpinnedItems = [];
|
||||
for (var v = 0; v < newItems.length; v++) {
|
||||
const cand = newItems[v];
|
||||
let isPinned = false;
|
||||
for (var f = 0; f < filteredItems.length; f++) {
|
||||
if (filteredItems[f] === cand) {
|
||||
isPinned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isPinned)
|
||||
unpinnedItems.push(cand);
|
||||
}
|
||||
dropdownItems = unpinnedItems;
|
||||
} else {
|
||||
// No pinned items: all items go to drawer (none inline)
|
||||
filteredItems = [];
|
||||
dropdownItems = newItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilteredItems() {
|
||||
updateDebounceTimer.restart();
|
||||
}
|
||||
|
||||
function wildCardMatch(str, rule) {
|
||||
if (!str || !rule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First, convert '*' to a placeholder to preserve it, then escape other special regex characters
|
||||
// Use a unique placeholder that won't appear in normal strings
|
||||
const placeholder = '\uE000'; // Private use character
|
||||
let processedRule = rule.replace(/\*/g, placeholder);
|
||||
// Escape all special regex characters (but placeholder won't match this)
|
||||
let escapedRule = processedRule.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
// Convert placeholder back to '.*' for wildcard matching
|
||||
let pattern = escapedRule.replace(new RegExp(placeholder, 'g'), '.*');
|
||||
// Add ^ and $ to match the entire string
|
||||
pattern = '^' + pattern + '$';
|
||||
|
||||
try {
|
||||
const regex = new RegExp(pattern, 'i');
|
||||
// 'i' for case-insensitive
|
||||
return regex.test(str);
|
||||
} catch (e) {
|
||||
Logger.w("Tray", "Invalid regex pattern for wildcard match:", rule, e.message);
|
||||
return false; // If regex is invalid, it won't match
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDrawer(button) {
|
||||
TooltipService.hideImmediately();
|
||||
|
||||
// Close the popup menu if it's open
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
|
||||
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
|
||||
if (panel) {
|
||||
panel.widgetSection = root.section;
|
||||
panel.widgetIndex = root.sectionWidgetIndex;
|
||||
panel.toggle(this);
|
||||
}
|
||||
}
|
||||
|
||||
function onLoaded() {
|
||||
// When the widget is fully initialized with its props set the screen for the trayMenu
|
||||
if (trayMenu && trayMenu.item) {
|
||||
trayMenu.item.screen = screen;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SystemTray.items
|
||||
function onValuesChanged() {
|
||||
root.updateFilteredItems();
|
||||
// Repeater will automatically update when items change
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings
|
||||
function onSettingsSaved() {
|
||||
root.updateFilteredItems();
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for hidePassive changes to update filtering immediately
|
||||
onHidePassiveChanged: {
|
||||
root.updateFilteredItems();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.updateFilteredItems(); // Initial update
|
||||
}
|
||||
|
||||
// Content dimensions for implicit sizing
|
||||
readonly property int visibleItemCount: (root.drawerEnabled && dropdownItems.length > 0 ? 1 : 0) + filteredItems.length
|
||||
readonly property real capsulePadding: 0
|
||||
readonly property real capsuleWidth: isVertical ? capsuleHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2)
|
||||
readonly property real capsuleContentHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : capsuleHeight
|
||||
|
||||
implicitWidth: isVertical ? barHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2)
|
||||
implicitHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : barHeight
|
||||
visible: filteredItems.length > 0 || dropdownItems.length > 0
|
||||
opacity: (filteredItems.length > 0 || dropdownItems.length > 0) ? 1.0 : 0.0
|
||||
|
||||
// Visual capsule centered in parent
|
||||
Rectangle {
|
||||
id: visualCapsule
|
||||
width: capsuleWidth
|
||||
height: capsuleContentHeight
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
radius: Style.radiusM
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: chevronContextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
chevronContextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: trayFlow
|
||||
spacing: 0
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
// Position centered in capsule
|
||||
anchors.centerIn: visualCapsule
|
||||
|
||||
// Drawer opener (before items if opposite direction)
|
||||
NIconButton {
|
||||
id: chevronIconBefore
|
||||
visible: root.drawerEnabled && dropdownItems.length > 0 && BarService.getPillDirection(root)
|
||||
width: isVertical ? barHeight : capsuleHeight
|
||||
height: isVertical ? capsuleHeight : barHeight
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("trayDrawerPanel", root.screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
return I18n.tr("tooltips.open-tray-dropdown");
|
||||
}
|
||||
}
|
||||
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
|
||||
baseSize: capsuleHeight
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: "transparent"
|
||||
colorFg: root.chevronColor
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
icon: {
|
||||
switch (barPosition) {
|
||||
case "bottom":
|
||||
return "caret-up";
|
||||
case "left":
|
||||
return "caret-right";
|
||||
case "right":
|
||||
return "caret-left";
|
||||
case "top":
|
||||
default:
|
||||
return "caret-down";
|
||||
}
|
||||
}
|
||||
onClicked: toggleDrawer(this)
|
||||
onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen)
|
||||
}
|
||||
|
||||
// Pinned items
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: root.filteredItems
|
||||
|
||||
delegate: Item {
|
||||
id: trayDelegate
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: isVertical ? barHeight : capsuleHeight
|
||||
height: isVertical ? capsuleHeight : barHeight
|
||||
visible: modelData
|
||||
readonly property bool isHovered: root.hoveredItemIndex === index
|
||||
|
||||
// Tooltip anchor representing the visual area (for proper tooltip positioning)
|
||||
Item {
|
||||
id: tooltipAnchor
|
||||
width: capsuleHeight
|
||||
height: capsuleHeight
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
asynchronous: true
|
||||
backer.fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: {
|
||||
let icon = modelData?.icon || "";
|
||||
if (!icon) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Process icon path
|
||||
if (icon.includes("?path=")) {
|
||||
const chunks = icon.split("?path=");
|
||||
const name = chunks[0];
|
||||
const path = chunks[1];
|
||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
return `file://${path}/${fileName}`;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
|
||||
layer.enabled: widgetSettings.colorizeIcons !== false
|
||||
layer.effect: ShaderEffect {
|
||||
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
|
||||
property real colorizeMode: 1.0
|
||||
|
||||
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: hoverIndicator
|
||||
anchors.bottom: trayIcon.bottom
|
||||
anchors.bottomMargin: -2
|
||||
anchors.horizontalCenter: trayIcon.horizontalCenter
|
||||
width: Style.toOdd(iconSize * 0.25)
|
||||
height: 4
|
||||
color: trayDelegate.isHovered ? Color.mHover : "transparent"
|
||||
radius: Math.min(Style.radiusXXS, width / 2)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
root.hoveredItemIndex = trayDelegate.index;
|
||||
TooltipService.show(tooltipAnchor, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
|
||||
} else if (root.hoveredItemIndex === trayDelegate.index) {
|
||||
root.hoveredItemIndex = -1;
|
||||
TooltipService.hide(tooltipAnchor);
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (!modelData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Close any open menu first
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
|
||||
if (!modelData.onlyMenu) {
|
||||
modelData.activate();
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
// Close the menu if it was visible
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
return;
|
||||
}
|
||||
modelData.secondaryActivate && modelData.secondaryActivate();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
TooltipService.hideImmediately();
|
||||
|
||||
// Close the menu if it was visible
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any opened panel
|
||||
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
|
||||
PanelService.openedPanel.close();
|
||||
}
|
||||
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu && trayMenu.item) {
|
||||
// Calculate menu position after ensuring menu is loaded
|
||||
const calculateAndShow = () => {
|
||||
// Position menu based on bar position, using tooltipAnchor for proper positioning
|
||||
// Increased spacing for better alignment with other context menus
|
||||
let menuX, menuY;
|
||||
if (barPosition === "left") {
|
||||
// For left bar: position menu to the right of the visual area
|
||||
menuX = tooltipAnchor.width + Style.marginL;
|
||||
menuY = 0;
|
||||
} else if (barPosition === "right") {
|
||||
// For right bar: position menu to the left of the visual area
|
||||
menuX = -trayMenu.item.implicitWidth - Style.marginL;
|
||||
menuY = 0;
|
||||
} else {
|
||||
// For horizontal bars: center horizontally and position below visual area
|
||||
menuX = (tooltipAnchor.width / 2) - (trayMenu.item.implicitWidth / 2);
|
||||
menuY = tooltipAnchor.height + Style.marginS;
|
||||
}
|
||||
|
||||
PanelService.showTrayMenu(root.screen, modelData, trayMenu.item, tooltipAnchor, menuX, menuY, root.section, root.sectionWidgetIndex);
|
||||
};
|
||||
|
||||
// Use Qt.callLater to ensure menu dimensions are calculated
|
||||
Qt.callLater(calculateAndShow);
|
||||
} else {
|
||||
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drawer opener (after items if normal direction)
|
||||
NIconButton {
|
||||
id: chevronIconAfter
|
||||
visible: root.drawerEnabled && dropdownItems.length > 0 && !BarService.getPillDirection(root)
|
||||
width: isVertical ? barHeight : capsuleHeight
|
||||
height: isVertical ? capsuleHeight : barHeight
|
||||
tooltipText: I18n.tr("tooltips.open-tray-dropdown")
|
||||
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
|
||||
baseSize: capsuleHeight
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: "transparent"
|
||||
colorFg: root.chevronColor
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
icon: {
|
||||
switch (barPosition) {
|
||||
case "bottom":
|
||||
return "caret-up";
|
||||
case "left":
|
||||
return "caret-right";
|
||||
case "right":
|
||||
return "caret-left";
|
||||
case "top":
|
||||
default:
|
||||
return "caret-down";
|
||||
}
|
||||
}
|
||||
onClicked: toggleDrawer(this)
|
||||
onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen)
|
||||
}
|
||||
} // closes Flow
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: {
|
||||
const items = [];
|
||||
const active = VPNService.activeConnections;
|
||||
for (let i = 0; i < active.length; ++i) {
|
||||
const conn = active[i];
|
||||
items.push({
|
||||
"label": I18n.tr("actions.disconnect-vpn", {
|
||||
"name": conn.name
|
||||
}),
|
||||
"action": "disconnect:" + conn.uuid,
|
||||
"icon": "shield-off"
|
||||
});
|
||||
}
|
||||
const inactive = VPNService.inactiveConnections;
|
||||
for (let i = 0; i < inactive.length; ++i) {
|
||||
const conn = inactive[i];
|
||||
items.push({
|
||||
"label": I18n.tr("actions.connect-vpn", {
|
||||
"name": conn.name
|
||||
}),
|
||||
"action": "connect:" + conn.uuid,
|
||||
"icon": "shield-lock"
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
return;
|
||||
}
|
||||
if (action.startsWith("connect:")) {
|
||||
const uuid = action.substring("connect:".length);
|
||||
VPNService.connect(uuid);
|
||||
return;
|
||||
}
|
||||
if (action.startsWith("disconnect:")) {
|
||||
const uuid = action.substring("disconnect:".length);
|
||||
VPNService.disconnect(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: VPNService.hasActiveConnection ? "shield-lock" : "shield"
|
||||
text: {
|
||||
if (VPNService.activeConnections.length > 0) {
|
||||
return VPNService.activeConnections[0].name;
|
||||
}
|
||||
if (VPNService.connectingUuid) {
|
||||
const pending = VPNService.connections[VPNService.connectingUuid];
|
||||
if (pending) {
|
||||
return pending.name;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
suffix: {
|
||||
if (VPNService.activeConnections.length > 1) {
|
||||
return ` + ${VPNService.activeConnections.length - 1}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
autoHide: false
|
||||
forceOpen: !isBarVertical && root.displayMode === "alwaysShow"
|
||||
forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
tooltipText: {
|
||||
if (pill.text !== "") {
|
||||
return pill.text;
|
||||
}
|
||||
return I18n.tr("tooltips.manage-vpn");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings
|
||||
import qs.Services.Media
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
|
||||
readonly property string middleClickCommand: (widgetSettings.middleClickCommand !== undefined) ? widgetSettings.middleClickCommand : widgetMetadata.middleClickCommand
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor
|
||||
readonly property bool reverseScroll: Settings.data.general.reverseScroll
|
||||
|
||||
// Used to avoid opening the pill on Quickshell startup
|
||||
property bool firstVolumeReceived: false
|
||||
property int wheelAccumulator: 0
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
|
||||
// Connection used to open the pill when volume changes
|
||||
Connections {
|
||||
target: AudioService
|
||||
function onVolumeChanged() {
|
||||
// Logger.i("Bar:Volume", "onVolumeChanged")
|
||||
if (!firstVolumeReceived) {
|
||||
// Ignore the first volume change
|
||||
firstVolumeReceived = true;
|
||||
} else {
|
||||
// Hide any tooltip while the pill is visible / being updated
|
||||
TooltipService.hide();
|
||||
pill.show();
|
||||
externalHideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
if (!firstVolumeReceived) {
|
||||
firstVolumeReceived = true;
|
||||
} else {
|
||||
TooltipService.hide();
|
||||
pill.show();
|
||||
externalHideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function onVolumeAtMaximum() {
|
||||
if (!firstVolumeReceived) {
|
||||
firstVolumeReceived = true;
|
||||
} else {
|
||||
// Hide any tooltip while the pill is visible / being updated
|
||||
TooltipService.hide();
|
||||
pill.show();
|
||||
externalHideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function onVolumeAtMinimum() {
|
||||
if (!firstVolumeReceived) {
|
||||
firstVolumeReceived = true;
|
||||
} else {
|
||||
// Hide any tooltip while the pill is visible / being updated
|
||||
TooltipService.hide();
|
||||
pill.show();
|
||||
externalHideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: externalHideTimer
|
||||
running: false
|
||||
interval: 1500
|
||||
onTriggered: {
|
||||
pill.hide();
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.toggle-mute"),
|
||||
"action": "toggle-mute",
|
||||
"icon": AudioService.muted ? "volume-off" : "volume"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.run-custom-command"),
|
||||
"action": "custom-command",
|
||||
"icon": "adjustments"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "toggle-mute") {
|
||||
AudioService.setOutputMuted(!AudioService.muted);
|
||||
} else if (action === "custom-command") {
|
||||
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
customIconColor: Color.resolveColorKeyOptional(root.iconColorKey)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.textColorKey)
|
||||
icon: AudioService.getOutputIcon()
|
||||
autoHide: false // Important to be false so we can hover as long as we want
|
||||
text: {
|
||||
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
|
||||
const displayVolume = Math.min(maxVolume, AudioService.volume);
|
||||
return Math.round(displayVolume * 100);
|
||||
}
|
||||
suffix: "%"
|
||||
forceOpen: displayMode === "alwaysShow"
|
||||
forceClose: displayMode === "alwaysHide"
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("audioPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
const nick = AudioService.sink?.nickname ?? "";
|
||||
const volumeText = I18n.tr("tooltips.volume-at", {
|
||||
"volume": (() => {
|
||||
const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0;
|
||||
const displayVolume = Math.min(maxVolume, AudioService.volume);
|
||||
return Math.round(displayVolume * 100);
|
||||
})()
|
||||
});
|
||||
return nick ? volumeText + "\n" + nick : volumeText;
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: function (delta) {
|
||||
// Hide tooltip as soon as the user starts scrolling to adjust volume
|
||||
TooltipService.hide();
|
||||
|
||||
if (root.reverseScroll)
|
||||
delta *= -1;
|
||||
|
||||
wheelAccumulator += delta;
|
||||
if (wheelAccumulator >= 120) {
|
||||
wheelAccumulator = 0;
|
||||
AudioService.increaseVolume();
|
||||
} else if (wheelAccumulator <= -120) {
|
||||
wheelAccumulator = 0;
|
||||
AudioService.decreaseVolume();
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
PanelService.getPanel("audioPanel", screen)?.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["sh", "-c", middleClickCommand]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor
|
||||
|
||||
enabled: Settings.data.wallpaper.enabled
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
icon: "wallpaper-selector"
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("wallpaperPanel", screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
return I18n.tr("tooltips.wallpaper-selector");
|
||||
}
|
||||
}
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: Color.resolveColorKey(iconColorKey)
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.random-wallpaper"),
|
||||
"action": "random-wallpaper",
|
||||
"icon": "dice"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "random-wallpaper") {
|
||||
WallpaperService.setRandomWallpaper();
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen);
|
||||
if (Settings.data.wallpaper.panelPosition === "follow_bar") {
|
||||
wallpaperPanel?.toggle(this);
|
||||
} else {
|
||||
wallpaperPanel?.toggle();
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user