add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,252 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
property var entriesModel: []
property var updateEntry
property var reorderEntries
property var openEntrySettingsDialog
// List of items
Item {
Layout.fillWidth: true
implicitHeight: listView.contentHeight
NListView {
id: listView
anchors.fill: parent
spacing: Style.marginS
interactive: false
reserveScrollbarSpace: false
model: root.entriesModel
delegate: Item {
id: delegateItem
width: listView.availableWidth
height: contentRow.height
required property int index
required property var modelData
property bool dragging: false
property int dragStartY: 0
property int dragStartIndex: -1
property int dragTargetIndex: -1
Rectangle {
anchors.fill: parent
radius: Style.radiusM
color: delegateItem.dragging ? Color.mSurfaceVariant : "transparent"
border.color: delegateItem.dragging ? Color.mOutline : "transparent"
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
RowLayout {
id: contentRow
width: parent.width
spacing: Style.marginS
// Drag handle
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : "transparent"
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 2
Repeater {
model: 3
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.28
Layout.preferredHeight: 2
radius: 1
color: Color.mOutline
}
}
}
MouseArea {
id: dragHandleMouseArea
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
hoverEnabled: true
preventStealing: false
z: 1000
onPressed: mouse => {
delegateItem.dragStartIndex = delegateItem.index;
delegateItem.dragTargetIndex = delegateItem.index;
delegateItem.dragStartY = delegateItem.y;
delegateItem.dragging = true;
delegateItem.z = 999;
preventStealing = true;
}
onPositionChanged: mouse => {
if (delegateItem.dragging) {
var dy = mouse.y - height / 2;
var newY = delegateItem.y + dy;
newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height));
delegateItem.y = newY;
var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + Style.marginS));
targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1));
delegateItem.dragTargetIndex = targetIndex;
}
}
onReleased: {
preventStealing = false;
if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) {
root.reorderEntries(delegateItem.dragStartIndex, delegateItem.dragTargetIndex);
}
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
onCanceled: {
preventStealing = false;
delegateItem.dragging = false;
delegateItem.dragStartIndex = -1;
delegateItem.dragTargetIndex = -1;
delegateItem.z = 0;
}
}
}
// Enable checkbox
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 0.7
Layout.preferredHeight: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusXS
color: modelData.enabled ? Color.mPrimary : Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NIcon {
visible: modelData.enabled
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
icon: "check"
color: Color.mOnPrimary
pointSize: Math.max(Style.fontSizeXS, Style.baseWidgetSize * 0.35)
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.updateEntry(index, {
"enabled": !modelData.enabled
});
}
}
}
// Label
NText {
Layout.fillWidth: true
text: modelData.text
color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Countdown toggle (only shown when global countdown is enabled)
NIconButtonHot {
visible: Settings.data.sessionMenu.enableCountdown
icon: "clock"
hot: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
tooltipText: I18n.tr("common.countdown")
onClicked: root.updateEntry(delegateItem.index, {
"countdownEnabled": !(modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true)
})
}
// Settings button (cogwheel)
NIconButton {
icon: "settings"
tooltipText: I18n.tr("panels.session-menu.entry-settings-tooltip")
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignVCenter
onClicked: root.openEntrySettingsDialog(delegateItem.index)
}
}
// Position binding for non-dragging state
y: {
if (delegateItem.dragging) {
return delegateItem.y;
}
var draggedIndex = -1;
var targetIndex = -1;
for (var i = 0; i < listView.count; i++) {
var item = listView.itemAtIndex(i);
if (item && item.dragging) {
draggedIndex = item.dragStartIndex;
targetIndex = item.dragTargetIndex;
break;
}
}
if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) {
var currentIndex = delegateItem.index;
if (draggedIndex < targetIndex) {
if (currentIndex > draggedIndex && currentIndex <= targetIndex) {
return (currentIndex - 1) * (delegateItem.height + Style.marginS);
}
} else {
if (currentIndex >= targetIndex && currentIndex < draggedIndex) {
return (currentIndex + 1) * (delegateItem.height + Style.marginS);
}
}
}
return delegateItem.index * (delegateItem.height + Style.marginS);
}
Behavior on y {
enabled: !delegateItem.dragging
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
}
}
}
}
@@ -0,0 +1,122 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.large-buttons-style-label")
description: I18n.tr("panels.session-menu.large-buttons-style-description")
checked: Settings.data.sessionMenu.largeButtonsStyle
onToggled: checked => Settings.data.sessionMenu.largeButtonsStyle = checked
}
NComboBox {
visible: Settings.data.sessionMenu.largeButtonsStyle
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.large-buttons-layout-label")
description: I18n.tr("panels.session-menu.large-buttons-layout-description")
model: [
{
"key": "grid",
"name": I18n.tr("options.session-menu-grid-layout.grid")
},
{
"key": "single-row",
"name": I18n.tr("options.session-menu-grid-layout.single-row")
}
]
currentKey: Settings.data.sessionMenu.largeButtonsLayout
defaultValue: Settings.getDefaultValue("sessionMenu.largeButtonsLayout")
onSelected: key => Settings.data.sessionMenu.largeButtonsLayout = key
}
NComboBox {
label: I18n.tr("common.position")
description: I18n.tr("panels.session-menu.position-description")
Layout.fillWidth: true
model: [
{
"key": "center",
"name": I18n.tr("positions.center")
},
{
"key": "top_center",
"name": I18n.tr("positions.top-center")
},
{
"key": "top_left",
"name": I18n.tr("positions.top-left")
},
{
"key": "top_right",
"name": I18n.tr("positions.top-right")
},
{
"key": "bottom_center",
"name": I18n.tr("positions.bottom-center")
},
{
"key": "bottom_left",
"name": I18n.tr("positions.bottom-left")
},
{
"key": "bottom_right",
"name": I18n.tr("positions.bottom-right")
}
]
currentKey: Settings.data.sessionMenu.position
onSelected: key => Settings.data.sessionMenu.position = key
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.position")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.show-header-label")
description: I18n.tr("panels.session-menu.show-header-description")
checked: Settings.data.sessionMenu.showHeader
onToggled: checked => Settings.data.sessionMenu.showHeader = checked
visible: !Settings.data.sessionMenu.largeButtonsStyle
defaultValue: Settings.getDefaultValue("sessionMenu.showHeader")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.show-keybinds-label")
description: I18n.tr("panels.session-menu.show-keybinds-description")
checked: Settings.data.sessionMenu.showKeybinds
onToggled: checked => Settings.data.sessionMenu.showKeybinds = checked
defaultValue: Settings.getDefaultValue("sessionMenu.showKeybinds")
}
NToggle {
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.enable-countdown-label")
description: I18n.tr("panels.session-menu.enable-countdown-description")
checked: Settings.data.sessionMenu.enableCountdown
onToggled: checked => Settings.data.sessionMenu.enableCountdown = checked
defaultValue: Settings.getDefaultValue("sessionMenu.enableCountdown")
}
NValueSlider {
visible: Settings.data.sessionMenu.enableCountdown
Layout.fillWidth: true
label: I18n.tr("panels.session-menu.countdown-duration-label")
description: I18n.tr("panels.session-menu.countdown-duration-description")
from: 1000
to: 30000
stepSize: 1000
showReset: true
value: Settings.data.sessionMenu.countdownDuration
onMoved: value => Settings.data.sessionMenu.countdownDuration = value
text: Math.round(Settings.data.sessionMenu.countdownDuration / 1000) + "s"
defaultValue: Settings.getDefaultValue("sessionMenu.countdownDuration")
}
}
@@ -0,0 +1,186 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
// Session Menu Entry Settings Dialog Component
Popup {
id: root
property int entryIndex: -1
property var entryData: null
property string entryId: ""
property string entryText: ""
property string keybindInputText: ""
signal updateEntryProperties(int index, var properties)
// Default commands mapping
readonly property var defaultCommands: {
"lock": I18n.tr("panels.session-menu.entry-settings-default-command-lock"),
"suspend": "systemctl suspend || loginctl suspend",
"hibernate": "systemctl hibernate || loginctl hibernate",
"reboot": "systemctl reboot || loginctl reboot",
"rebootToUefi": "systemctl reboot --firmware-setup || loginctl reboot --firmware-setup",
"logout": I18n.tr("panels.session-menu.entry-settings-default-command-logout"),
"shutdown": "systemctl poweroff || loginctl poweroff",
"userspaceReboot": "systemctl soft-reboot"
}
readonly property string defaultCommand: defaultCommands[entryId] || ""
width: Math.max(content.implicitWidth + padding * 2, 500)
height: content.implicitHeight + padding * 2
padding: Style.marginXL
modal: true
closePolicy: Popup.NoAutoClose
dim: false
anchors.centerIn: parent
onOpened: {
// Load command when popup opens
if (entryData) {
commandInput.text = entryData.command || "";
keybindInputText = entryData.keybind || "";
}
// Request focus to ensure keyboard input works
forceActiveFocus();
}
function save() {
root.updateEntryProperties(root.entryIndex, {
"command": commandInput.text,
"keybind": keybindInputText
});
}
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL
border.color: Color.mPrimary
border.width: Style.borderM
}
contentItem: FocusScope {
id: focusScope
focus: true
ColumnLayout {
id: content
anchors.fill: parent
spacing: Style.marginM
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: I18n.tr("panels.session-menu.entry-settings-title", {
"entry": root.entryText
})
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
onClicked: {
root.save();
root.close();
}
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Command input
NTextInput {
id: commandInput
Layout.fillWidth: true
label: I18n.tr("common.command")
description: I18n.tr("panels.session-menu.entry-settings-command-description")
placeholderText: I18n.tr("panels.session-menu.entry-settings-command-placeholder")
onTextChanged: root.save()
}
// Default command info
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NLabel {
label: I18n.tr("panels.session-menu.entry-settings-default-info-label")
description: I18n.tr("panels.session-menu.entry-settings-default-info-description")
Layout.fillWidth: true
}
// Default command display
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: defaultCommandText.implicitHeight + Style.margin2M
radius: Style.radiusM
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
NIcon {
icon: "info"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
}
NText {
id: defaultCommandText
Layout.fillWidth: true
text: root.defaultCommand
color: Color.mOnSurfaceVariant
font.family: "monospace"
font.pointSize: Style.fontSizeS
wrapMode: Text.Wrap
}
}
}
}
NKeybindRecorder {
id: keybindRecorder
Layout.fillWidth: true
label: I18n.tr("common.keybind")
description: I18n.tr("placeholders.keybind-recording")
allowEmpty: true
maxKeybinds: 1
requireModifierForNormalKeys: false
currentKeybinds: keybindInputText ? [keybindInputText] : []
settingsPath: "sessionMenu.powerOptions[" + root.entryIndex + "].keybind"
onKeybindsChanged: newKeybinds => {
keybindInputText = newKeybinds.length > 0 ? newKeybinds[0] : "";
root.save();
}
}
// Hidden property to store the text since NKeybindRecorder manages its own state
// but we need to initialize it and read from it
// Bottom spacer to maintain padding
Item {
Layout.preferredHeight: Style.marginS
}
}
}
}
@@ -0,0 +1,266 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
property var _activeDialog: null
property list<var> entriesModel: []
property list<var> entriesDefault: [
{
"id": "lock",
"text": I18n.tr("common.lock"),
"enabled": true,
"required": false
},
{
"id": "suspend",
"text": I18n.tr("common.suspend"),
"enabled": true,
"required": false
},
{
"id": "hibernate",
"text": I18n.tr("common.hibernate"),
"enabled": true,
"required": false
},
{
"id": "reboot",
"text": I18n.tr("common.reboot"),
"enabled": true,
"required": false
},
{
"id": "userspaceReboot",
"text": I18n.tr("common.userspace-reboot"),
"enabled": false,
"required": false
},
{
"id": "rebootToUefi",
"text": I18n.tr("common.reboot-to-uefi"),
"enabled": true,
"required": false
},
{
"id": "logout",
"text": I18n.tr("common.logout"),
"enabled": true,
"required": false
},
{
"id": "shutdown",
"text": I18n.tr("common.shutdown"),
"enabled": true,
"required": false
}
]
function saveEntries() {
var toSave = [];
var enabledNumber = 1;
for (var i = 0; i < entriesModel.length; i++) {
var keybind = entriesModel[i].keybind || "";
if (entriesModel[i].enabled) {
// For enabled entries with numeric or empty keybinds, assign sequential number
if (keybind === "" || /^\d+$/.test(keybind)) {
keybind = String(enabledNumber);
}
enabledNumber++;
} else {
// For disabled entries with numeric keybinds, clear them
if (/^\d+$/.test(keybind)) {
keybind = "";
}
}
toSave.push({
"action": entriesModel[i].id,
"enabled": entriesModel[i].enabled,
"countdownEnabled": entriesModel[i].countdownEnabled !== undefined ? entriesModel[i].countdownEnabled : true,
"command": entriesModel[i].command || "",
"keybind": keybind
});
}
// Update local model with renumbered keybinds
var newModel = [];
for (var i = 0; i < entriesModel.length; i++) {
newModel.push(Object.assign({}, entriesModel[i], {
"keybind": toSave[i].keybind
}));
}
entriesModel = newModel;
Settings.data.sessionMenu.powerOptions = toSave;
}
function updateEntry(idx, properties) {
var newModel = entriesModel.slice();
newModel[idx] = Object.assign({}, newModel[idx], properties);
entriesModel = newModel;
saveEntries();
}
function reorderEntries(fromIndex, toIndex) {
var newModel = entriesModel.slice();
var item = newModel.splice(fromIndex, 1)[0];
newModel.splice(toIndex, 0, item);
entriesModel = newModel;
saveEntries();
}
Component.onDestruction: {
if (_activeDialog && _activeDialog.close) {
var dialog = _activeDialog;
_activeDialog = null;
dialog.close();
dialog.destroy();
}
}
function openEntrySettingsDialog(index) {
if (index < 0 || index >= entriesModel.length) {
return;
}
var entry = entriesModel[index];
var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml");
function instantiateAndOpen() {
if (root._activeDialog) {
root._activeDialog.close();
root._activeDialog.destroy();
root._activeDialog = null;
}
var dialog = component.createObject(Overlay.overlay, {
"entryIndex": index,
"entryData": entry,
"entryId": entry.id,
"entryText": entry.text
});
if (dialog) {
root._activeDialog = dialog;
dialog.updateEntryProperties.connect((idx, properties) => {
root.updateEntry(idx, properties);
});
dialog.closed.connect(() => {
if (root._activeDialog === dialog) {
root._activeDialog = null;
dialog.destroy();
}
});
dialog.open();
} else {
Logger.e("SessionMenuTab", "Failed to create entry settings dialog");
}
}
if (component.status === Component.Ready) {
instantiateAndOpen();
} else if (component.status === Component.Error) {
Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString());
} else {
component.statusChanged.connect(function () {
if (component.status === Component.Ready) {
instantiateAndOpen();
} else if (component.status === Component.Error) {
Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString());
}
});
}
}
Component.onCompleted: {
entriesModel = [];
// Add the entries available in settings
for (var i = 0; i < Settings.data.sessionMenu.powerOptions.length; i++) {
const settingEntry = Settings.data.sessionMenu.powerOptions[i];
for (var j = 0; j < entriesDefault.length; j++) {
if (settingEntry.action === entriesDefault[j].id) {
var entry = entriesDefault[j];
entry.enabled = settingEntry.enabled;
// Default countdownEnabled to true for backward compatibility
entry.countdownEnabled = settingEntry.countdownEnabled !== undefined ? settingEntry.countdownEnabled : true;
// Load custom command if defined
entry.command = settingEntry.command || "";
entry.keybind = settingEntry.keybind || "";
entriesModel.push(entry);
}
}
}
// Add any missing entries from default
for (var i = 0; i < entriesDefault.length; i++) {
var found = false;
for (var j = 0; j < entriesModel.length; j++) {
if (entriesModel[j].id === entriesDefault[i].id) {
found = true;
break;
}
}
if (!found) {
var entry = entriesDefault[i];
// Default countdownEnabled to true for new entries
entry.countdownEnabled = true;
// Default command to empty string for new entries
entry.command = "";
entry.keybind = "";
entriesModel.push(entry);
}
}
saveEntries();
}
NTabBar {
id: subTabBar
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM
distributeEvenly: true
currentIndex: tabView.currentIndex
NTabButton {
text: I18n.tr("common.general")
tabIndex: 0
checked: subTabBar.currentIndex === 0
}
NTabButton {
text: I18n.tr("common.actions")
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.marginL
}
NTabView {
id: tabView
currentIndex: subTabBar.currentIndex
GeneralSubTab {}
ActionsSubTab {
entriesModel: root.entriesModel
updateEntry: root.updateEntry
reorderEntries: root.reorderEntries
openEntrySettingsDialog: root.openEntrySettingsDialog
}
}
}