1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 10:32:07 -04:00

Compare commits

...

9 Commits

Author SHA1 Message Date
bbedward
f0be36062e dankbar: guard against nil screen names 2026-03-16 11:32:59 -04:00
bbedward
9578d6daf9 popout: fix focusing of password prompts when popout is open
undesired effect of closing the popout but its probably the best
solution
2026-03-16 11:30:31 -04:00
bbedward
cc6766135d focused app: fallback to app name if no title in compact mode
fixes #2005
2026-03-16 11:25:50 -04:00
bbedward
28c9bb0925 cc: fix invalid number displays on percentages
fixes #2010
2026-03-16 11:18:42 -04:00
Ron Harel
7826d827dd feat: add configurable control center group ordering (#2006)
* Add grouped element reordering to control center setting popup.

Reorganize the control center widget menu into grouped rows and add drag handles for reordering.
Introduce controlCenterGroups to drive the grouped popup layout, along with dynamic content width calculation.
Disable dependent options when their parent icon is turned off, and refine DankToggle disabled colors to better distinguish checked and unchecked states.

* Apply Control Center group order to live widget rendering.

Apply persisted `controlCenterGroupOrder` to the actual control center button rendering path instead of only using it in the settings UI.
This refactors `ControlCenterButton.qml` to derive a normalized effective group order, build a small render model from that order, and use model-driven rendering for both vertical and horizontal layouts.

Highlights:
- add a default control center group order and normalize saved order data
- ignore unknown ids, deduplicate duplicates, and append missing known groups
- add shared group visibility helpers and derive a render model from them
- render both vertical and horizontal indicators from the effective order
- preserve existing icon, color, percent text, and visibility behavior
- keep the fallback settings icon outside persisted ordering
- reconnect cached interaction refs for audio, mic, and brightness to the real rendered group containers so wheel and right-click behavior still work
- clear and refresh interaction refs on orientation, visibility, and delegate lifecycle changes
- tighten horizontal composite group sizing by measuring actual rendered content, fixing extra spacing around the audio indicator

Also updates the settings widgets UI to persist and restore control center group ordering consistently with the live control center rendering.
2026-03-16 11:11:26 -04:00
Michael Erdely
7f392acc54 Implement ability to cycle through launcher modes (#2003)
Use Ctrl+Left/Right and Ctrl+H/L to move back and forward through the
modes of the launcher
2026-03-16 11:08:07 -04:00
Michael Erdely
190fd662ad Implement more intuitive keybinds for Launcher (#2002)
With programs like rofi, pressing the tab key advances to the next item
in the list. This change makes the Launcher behave in the same way,
moving the action cycling to Ctrl+Tab (and Ctrl+Shift+Tab for reverse.
2026-03-16 11:07:25 -04:00
Triệu Kha
e18587c471 feat(calendar): add show week number option (#1990)
* increase DankDashPopout width to accommodate week number column

* add getWeekNumber function

* add week number column

* add showWeekNumber SettingsData

* add showWeekNumber SettingsSpec

* make dash popout width changes reponsively to showWeekNumber option

* complete and cleanup

* fix typo

* fix typo
2026-03-16 11:06:21 -04:00
Walid Salah
ddb079b62d Add terminal multiplexer launcher (#1687)
* Add tmux

* Add mux modal

* Restore the settings config version

* Revert typo

* Use DankModal for InputModal

* Simplify terminal flags

* use showWithOptions for inputModals instead

* Fix translation

* use Quickshell.env("TERMINAL") to choose terminal

* Fix typo

* Hide muxModal after creating new session

* Add mux check, moved exclusion to service, And use ScriptModel

* Revert unrelated change

* Add blank line
2026-03-16 11:05:16 -04:00
30 changed files with 7323 additions and 4287 deletions

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property int settingsConfigVersion: 6 readonly property int settingsConfigVersion: 5
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -150,6 +150,7 @@ Singleton {
property int mangoLayoutBorderSize: -1 property int mangoLayoutBorderSize: -1
property int firstDayOfWeek: -1 property int firstDayOfWeek: -1
property bool showWeekNumber: false
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
@@ -453,6 +454,11 @@ Singleton {
property bool syncModeWithPortal: true property bool syncModeWithPortal: true
property bool terminalsAlwaysDark: false property bool terminalsAlwaysDark: false
property string muxType: "tmux"
property bool muxUseCustomCommand: false
property string muxCustomCommand: ""
property string muxSessionFilter: ""
property bool runDmsMatugenTemplates: true property bool runDmsMatugenTemplates: true
property bool matugenTemplateGtk: true property bool matugenTemplateGtk: true
property bool matugenTemplateNiri: true property bool matugenTemplateNiri: true

View File

@@ -33,6 +33,7 @@ var SPEC = {
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
firstDayOfWeek: { def: -1 }, firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false },
use24HourClock: { def: true }, use24HourClock: { def: true },
showSeconds: { def: false }, showSeconds: { def: false },
padHours12Hour: { def: false }, padHours12Hour: { def: false },
@@ -268,6 +269,11 @@ var SPEC = {
syncModeWithPortal: { def: true }, syncModeWithPortal: { def: true },
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" }, terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
muxType: { def: "tmux" },
muxUseCustomCommand: { def: false },
muxCustomCommand: { def: "" },
muxSessionFilter: { def: "" },
runDmsMatugenTemplates: { def: true }, runDmsMatugenTemplates: { def: true },
matugenTemplateGtk: { def: true }, matugenTemplateGtk: { def: true },
matugenTemplateNiri: { def: true }, matugenTemplateNiri: { def: true },

View File

@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
import qs.Modals.Greeter import qs.Modals.Greeter
import qs.Modals.Settings import qs.Modals.Settings
import qs.Modals.DankLauncherV2 import qs.Modals.DankLauncherV2
import qs.Modals
import qs.Modules import qs.Modules
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Modules.DankDash import qs.Modules.DankDash
@@ -619,6 +620,10 @@ Item {
} }
} }
MuxModal {
id: muxModal
}
ClipboardHistoryModal { ClipboardHistoryModal {
id: clipboardHistoryModalPopup id: clipboardHistoryModalPopup

View File

@@ -0,0 +1,312 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:input-modal"
keepPopoutsOpen: true
property string inputTitle: ""
property string inputMessage: ""
property string inputPlaceholder: ""
property string inputText: ""
property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
property color confirmButtonColor: Theme.primary
property var onConfirm: function (text) {}
property var onCancel: function () {}
property int selectedButton: -1
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
inputTitle = title || "";
inputMessage = message || "";
inputPlaceholder = "";
inputText = "";
confirmButtonText = "Confirm";
cancelButtonText = "Cancel";
confirmButtonColor = Theme.primary;
onConfirm = onConfirmCallback || ((text) => {});
onCancel = onCancelCallback || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function showWithOptions(options) {
inputTitle = options.title || "";
inputMessage = options.message || "";
inputPlaceholder = options.placeholder || "";
inputText = options.initialText || "";
confirmButtonText = options.confirmText || "Confirm";
cancelButtonText = options.cancelText || "Cancel";
confirmButtonColor = options.confirmColor || Theme.primary;
onConfirm = options.onConfirm || ((text) => {});
onCancel = options.onCancel || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function confirmAndClose() {
const text = inputText;
close();
if (onConfirm) {
onConfirm(text);
}
}
function cancelAndClose() {
close();
if (onCancel) {
onCancel();
}
}
function selectButton() {
if (selectedButton === 0) {
cancelAndClose();
} else {
confirmAndClose();
}
}
shouldBeVisible: false
allowStacking: true
modalWidth: 350
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: cancelAndClose()
onOpened: {
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.textInputRef) {
contentLoader.item.textInputRef.forceActiveFocus();
}
});
}
content: Component {
FocusScope {
anchors.fill: parent
implicitHeight: mainColumn.implicitHeight
focus: true
property alias textInputRef: textInput
Keys.onPressed: function (event) {
const textFieldFocused = textInput.activeFocus;
switch (event.key) {
case Qt.Key_Escape:
root.cancelAndClose();
event.accepted = true;
break;
case Qt.Key_Tab:
if (textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
textInput.focus = false;
} else {
root.keyboardNavigation = true;
if (root.selectedButton === -1) {
root.selectedButton = 0;
} else if (root.selectedButton === 0) {
root.selectedButton = 1;
} else {
root.selectedButton = -1;
textInput.forceActiveFocus();
}
}
event.accepted = true;
break;
case Qt.Key_Left:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
event.accepted = true;
}
break;
case Qt.Key_Right:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 1;
event.accepted = true;
}
break;
case Qt.Key_Return:
case Qt.Key_Enter:
if (root.selectedButton !== -1) {
root.selectButton();
} else {
root.confirmAndClose();
}
event.accepted = true;
break;
}
}
Column {
id: mainColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingL
spacing: 0
StyledText {
text: root.inputTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
Item {
width: 1
height: Theme.spacingL
}
StyledText {
text: root.inputMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
visible: root.inputMessage !== ""
}
Item {
width: 1
height: root.inputMessage !== "" ? Theme.spacingL : 0
visible: root.inputMessage !== ""
}
Rectangle {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
border.color: textInput.activeFocus ? Theme.primary : "transparent"
border.width: textInput.activeFocus ? 1 : 0
TextInput {
id: textInput
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
selectionColor: Theme.primary
selectedTextColor: Theme.primaryText
clip: true
text: root.inputText
onTextChanged: root.inputText = text
StyledText {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
text: root.inputPlaceholder
visible: textInput.text === "" && !textInput.activeFocus
}
}
}
Item {
width: 1
height: Theme.spacingL * 1.5
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
if (root.keyboardNavigation && root.selectedButton === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (cancelButton.containsMouse) {
return Theme.surfacePressed;
} else {
return Theme.surfaceVariantAlpha;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
StyledText {
text: root.cancelButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.cancelAndClose()
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
const baseColor = root.confirmButtonColor;
if (root.keyboardNavigation && root.selectedButton === 1) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
} else if (confirmButton.containsMouse) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
} else {
return baseColor;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
StyledText {
text: root.confirmButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.confirmAndClose()
}
}
}
Item {
width: 1
height: Theme.spacingL
}
}
}
}
}

View File

@@ -207,9 +207,12 @@ Rectangle {
selectedActionIndex = 0; selectedActionIndex = 0;
} }
function cycleAction() { function cycleAction(reverse = false) {
if (actions.length > 0) { if (actions.length > 0) {
selectedActionIndex = (selectedActionIndex + 1) % actions.length; if (! reverse)
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
else
selectedActionIndex = (selectedActionIndex - 1) % actions.length;
ensureSelectedVisible(); ensureSelectedVisible();
} }
} }

View File

@@ -353,10 +353,13 @@ Item {
performSearch(); performSearch();
} }
function cycleMode() { function cycleMode(reverse = false) {
var modes = ["all", "apps", "files", "plugins"]; var modes = ["all", "apps", "files", "plugins"];
var currentIndex = modes.indexOf(searchMode); var currentIndex = modes.indexOf(searchMode);
var nextIndex = (currentIndex + 1) % modes.length; if (!reverse)
var nextIndex = (currentIndex + 1) % modes.length;
else
var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
setMode(modes[nextIndex]); setMode(modes[nextIndex]);
} }

View File

@@ -158,6 +158,10 @@ FocusScope {
controller.selectPageUp(8); controller.selectPageUp(8);
return; return;
case Qt.Key_Right: case Qt.Key_Right:
if (hasCtrl) {
controller.cycleMode();
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectRight(); controller.selectRight();
return; return;
@@ -165,12 +169,25 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Left: case Qt.Key_Left:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectLeft(); controller.selectLeft();
return; return;
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_H:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
event.accepted = false;
return;
case Qt.Key_J: case Qt.Key_J:
if (hasCtrl) { if (hasCtrl) {
controller.selectNext(); controller.selectNext();
@@ -185,6 +202,13 @@ FocusScope {
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_L:
if (hasCtrl) {
controller.cycleMode();
return;
}
event.accepted = false;
return;
case Qt.Key_N: case Qt.Key_N:
if (hasCtrl) { if (hasCtrl) {
controller.selectNextSection(); controller.selectNextSection();
@@ -200,13 +224,19 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Tab: case Qt.Key_Tab:
if (actionPanel.hasActions) { if (hasCtrl && actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
return;
} }
controller.selectNext();
return; return;
case Qt.Key_Backtab: case Qt.Key_Backtab:
if (actionPanel.expanded) if (hasCtrl && actionPanel.expanded) {
actionPanel.hide(); const reverse = true
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
return;
}
controller.selectPrevious();
return; return;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
@@ -388,7 +418,7 @@ FocusScope {
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Tab " + I18n.tr("actions") text: "Ctrl-Tab " + I18n.tr("actions")
font.pixelSize: Theme.fontSizeSmall - 1 font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
visible: actionPanel.hasActions visible: actionPanel.hasActions

View File

@@ -0,0 +1,621 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: muxModal
layerNamespace: "dms:mux"
property int selectedIndex: -1
property string searchText: ""
property var filteredSessions: []
function updateFilteredSessions() {
var filtered = []
var lowerSearch = searchText.trim().toLowerCase()
for (var i = 0; i < MuxService.sessions.length; i++) {
var session = MuxService.sessions[i]
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
continue
filtered.push(session)
}
filteredSessions = filtered
if (selectedIndex >= filteredSessions.length) {
selectedIndex = Math.max(0, filteredSessions.length - 1)
}
}
onSearchTextChanged: updateFilteredSessions()
Connections {
target: MuxService
function onSessionsChanged() {
updateFilteredSessions()
}
}
HyprlandFocusGrab {
id: grab
windows: [muxModal.contentWindow]
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
}
function toggle() {
if (shouldBeVisible) {
hide()
} else {
show()
}
}
function show() {
open()
selectedIndex = -1
searchText = ""
MuxService.refreshSessions()
shouldHaveFocus = true
Qt.callLater(() => {
if (muxPanel && muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
})
}
function hide() {
close()
selectedIndex = -1
searchText = ""
}
function attachToSession(name) {
MuxService.attachToSession(name)
hide()
}
function renameSession(name) {
inputModal.showWithOptions({
title: I18n.tr("Rename Session"),
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
initialText: name,
onConfirm: function (newName) {
MuxService.renameSession(name, newName)
}
})
}
function killSession(name) {
confirmModal.showWithOptions({
title: I18n.tr("Kill Session"),
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
confirmText: I18n.tr("Kill"),
confirmColor: Theme.primary,
onConfirm: function () {
MuxService.killSession(name)
}
})
}
function createNewSession() {
inputModal.showWithOptions({
title: I18n.tr("New Session"),
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
onConfirm: function (name) {
MuxService.createSession(name)
hide()
}
})
}
function selectNext() {
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
}
function selectPrevious() {
selectedIndex = Math.max(selectedIndex - 1, -1)
}
function activateSelected() {
if (selectedIndex === -1) {
createNewSession()
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
attachToSession(filteredSessions[selectedIndex].name)
}
}
visible: false
modalWidth: 600
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
onBackgroundClicked: hide()
Timer {
interval: 3000
running: muxModal.shouldBeVisible
repeat: true
onTriggered: MuxService.refreshSessions()
}
IpcHandler {
function open(): string {
muxModal.show()
return "MUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "MUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "MUX_TOGGLE_SUCCESS"
}
target: "mux"
}
// Backwards compatibility
IpcHandler {
function open(): string {
muxModal.show()
return "TMUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "TMUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "TMUX_TOGGLE_SUCCESS"
}
target: "tmux"
}
InputModal {
id: inputModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
ConfirmModal {
id: confirmModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
directContent: Item {
id: muxPanel
clip: false
property alias searchField: searchField
Keys.onPressed: event => {
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Down)) {
selectNext()
event.accepted = true
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Up)) {
selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
createNewSession()
event.accepted = true
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
renameSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
killSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
hide()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
activateSelected()
event.accepted = true
}
}
Column {
width: parent.width - Theme.spacingM * 2
height: parent.height - Theme.spacingM * 2
x: Theme.spacingM
y: Theme.spacingM
spacing: Theme.spacingS
// Header
Item {
width: parent.width
height: 40
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("%1 active, %2 filtered").arg(MuxService.sessions.length).arg(muxModal.filteredSessions.length)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
// Search field
DankTextField {
id: searchField
width: parent.width
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search sessions...")
keyForwardTargets: [muxPanel]
onTextEdited: {
muxModal.searchText = text
muxModal.selectedIndex = 0
}
}
// New Session Button
Rectangle {
width: parent.width
height: 56
radius: Theme.cornerRadius
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: Theme.primaryContainer
DankIcon {
anchors.centerIn: parent
name: "add"
size: Theme.iconSize
color: Theme.primary
}
}
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: I18n.tr("New Session")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
MouseArea {
id: newMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.createNewSession()
}
}
// Sessions List
Rectangle {
width: parent.width
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
radius: Theme.cornerRadius
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
values: muxModal.filteredSessions
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 64
radius: Theme.cornerRadius
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
MouseArea {
id: sessionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.attachToSession(modelData.name)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
// Avatar
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
StyledText {
anchors.centerIn: parent
text: modelData.name.charAt(0).toUpperCase()
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: modelData.attached ? Theme.primary : Theme.surfaceText
}
}
// Info
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
text: {
var parts = []
if (modelData.windows !== "N/A")
parts.push(I18n.tr("%1 windows").arg(modelData.windows))
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
return parts.join(" \u2022 ")
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
// Rename button (tmux only)
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
visible: MuxService.supportsRename
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
DankIcon {
anchors.centerIn: parent
name: "edit"
size: Theme.iconSizeSmall
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: renameMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.renameSession(modelData.name)
}
}
// Delete button
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
DankIcon {
anchors.centerIn: parent
name: "delete"
size: Theme.iconSizeSmall
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
muxModal.killSession(modelData.name)
}
}
}
}
}
}
// Empty state
Item {
width: parent.width
height: muxModal.filteredSessions.length === 0 ? 200 : 0
visible: muxModal.filteredSessions.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
size: 48
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
// Shortcuts bar
Row {
id: shortcutsBar
width: parent.width
spacing: Theme.spacingM
bottomPadding: Theme.spacingS
Repeater {
model: {
var shortcuts = [
{ key: "↑↓", label: I18n.tr("Navigate") },
{ key: "↵", label: I18n.tr("Attach") },
{ key: "^N", label: I18n.tr("New") },
{ key: "^D", label: I18n.tr("Kill") },
{ key: "Esc", label: I18n.tr("Close") }
]
if (MuxService.supportsRename)
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
return shortcuts
}
delegate: Row {
required property var modelData
spacing: 4
Rectangle {
width: keyText.width + Theme.spacingS
height: keyText.height + 4
radius: 4
color: Theme.surfaceContainerHighest
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: keyText
anchors.centerIn: parent
text: modelData.key
font.pixelSize: Theme.fontSizeSmall - 1
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}

View File

@@ -503,5 +503,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
Loader {
id: muxLoader
anchors.fill: parent
active: root.currentIndex === 30
visible: active
focus: active
sourceComponent: MuxTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
} }
} }

View File

@@ -266,6 +266,12 @@ Rectangle {
"tabIndex": 8, "tabIndex": 8,
"cupsOnly": true "cupsOnly": true
}, },
{
"id": "multiplexers",
"text": I18n.tr("Multiplexers"),
"icon": "terminal",
"tabIndex": 30
},
{ {
"id": "window_rules", "id": "window_rules",
"text": I18n.tr("Window Rules"), "text": I18n.tr("Window Rules"),

View File

@@ -70,6 +70,16 @@ DankPopout {
backgroundInteractive: !anyModalOpen backgroundInteractive: !anyModalOpen
onCredentialsPromptOpenChanged: {
if (credentialsPromptOpen && shouldBeVisible)
close();
}
onPolkitModalOpenChanged: {
if (polkitModalOpen && shouldBeVisible)
close();
}
customKeyboardFocus: { customKeyboardFocus: {
if (!shouldBeVisible) if (!shouldBeVisible)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;

View File

@@ -289,7 +289,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -312,7 +312,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -336,7 +336,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -360,7 +360,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -686,6 +686,7 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal() onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() { function updateActivePopoutState() {
if (!barWindow.screen) return;
const screenName = barWindow.screen.name; const screenName = barWindow.screen.name;
const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];

View File

@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
@@ -38,12 +39,20 @@ BasePill {
property var _vAudio: null property var _vAudio: null
property var _vBrightness: null property var _vBrightness: null
property var _vMic: null property var _vMic: null
property var _interactionDelegates: []
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
readonly property var controlCenterRenderModel: getControlCenterRenderModel()
onIsVerticalOrientationChanged: root.clearInteractionRefs()
onWheel: function (wheelEvent) { onWheel: function (wheelEvent) {
const delta = wheelEvent.angleDelta.y; const delta = wheelEvent.angleDelta.y;
if (delta === 0) if (delta === 0)
return; return;
root.refreshInteractionRefs();
const rootX = wheelEvent.x - root.leftMargin; const rootX = wheelEvent.x - root.leftMargin;
const rootY = wheelEvent.y - root.topMargin; const rootY = wheelEvent.y - root.topMargin;
@@ -72,6 +81,8 @@ BasePill {
} }
onRightClicked: function (rootX, rootY) { onRightClicked: function (rootX, rootY) {
root.refreshInteractionRefs();
if (root.isVerticalOrientation && _vCol) { if (root.isVerticalOrientation && _vCol) {
const pos = root.mapToItem(_vCol, rootX, rootY); const pos = root.mapToItem(_vCol, rootX, rootY);
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) { if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
@@ -279,26 +290,142 @@ BasePill {
return CupsService.getTotalJobsNum() > 0; return CupsService.getTotalJobsNum() > 0;
} }
function getControlCenterIconSize() {
return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale);
}
function getEffectiveControlCenterGroupOrder() {
const knownIds = root.defaultControlCenterGroupOrder;
const savedOrder = root.widgetData?.controlCenterGroupOrder;
const result = [];
const seen = {};
if (savedOrder && typeof savedOrder.length === "number") {
for (let i = 0; i < savedOrder.length; ++i) {
const groupId = savedOrder[i];
if (knownIds.indexOf(groupId) === -1 || seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
}
for (let i = 0; i < knownIds.length; ++i) {
const groupId = knownIds[i];
if (seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
return result;
}
function isGroupVisible(groupId) {
switch (groupId) {
case "screenSharing":
return root.showScreenSharingIcon && NiriService.hasCasts;
case "network":
return root.showNetworkIcon && NetworkService.networkAvailable;
case "vpn":
return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected;
case "bluetooth":
return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled;
case "audio":
return root.showAudioIcon;
case "microphone":
return root.showMicIcon;
case "brightness":
return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice();
case "battery":
return root.showBatteryIcon && BatteryService.batteryAvailable;
case "printer":
return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
default:
return false;
}
}
function isCompositeGroup(groupId) {
return groupId === "audio" || groupId === "microphone" || groupId === "brightness";
}
function getControlCenterRenderModel() {
return root.effectiveControlCenterGroupOrder.map(groupId => ({
"id": groupId,
"visible": root.isGroupVisible(groupId),
"composite": root.isCompositeGroup(groupId)
}));
}
function clearInteractionRefs() {
root._hAudio = null;
root._hBrightness = null;
root._hMic = null;
root._vAudio = null;
root._vBrightness = null;
root._vMic = null;
}
function registerInteractionDelegate(isVertical, item) {
if (!item)
return;
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
if (entry && entry.item === item) {
entry.isVertical = isVertical;
return;
}
}
root._interactionDelegates = root._interactionDelegates.concat([
{
"isVertical": isVertical,
"item": item
}
]);
}
function unregisterInteractionDelegate(item) {
if (!item)
return;
root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item);
}
function refreshInteractionRefs() {
root.clearInteractionRefs();
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
const item = entry?.item;
if (!item || !item.visible)
continue;
const groupId = item.interactionGroupId;
if (entry.isVertical) {
if (groupId === "audio")
root._vAudio = item;
else if (groupId === "microphone")
root._vMic = item;
else if (groupId === "brightness")
root._vBrightness = item;
} else {
if (groupId === "audio")
root._hAudio = item;
else if (groupId === "microphone")
root._hMic = item;
else if (groupId === "brightness")
root._hBrightness = item;
}
}
}
function hasNoVisibleIcons() { function hasNoVisibleIcons() {
if (root.showScreenSharingIcon && NiriService.hasCasts) return !root.controlCenterRenderModel.some(entry => entry.visible);
return false;
if (root.showNetworkIcon && NetworkService.networkAvailable)
return false;
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
return false;
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
return false;
if (root.showAudioIcon)
return false;
if (root.showMicIcon)
return false;
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
return false;
if (root.showBatteryIcon && BatteryService.batteryAvailable)
return false;
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
return false;
return true;
} }
content: Component { content: Component {
@@ -309,12 +436,7 @@ BasePill {
Component.onCompleted: { Component.onCompleted: {
root._hRow = controlIndicators; root._hRow = controlIndicators;
root._vCol = controlColumn; root._vCol = controlColumn;
root._hAudio = audioIcon.parent; root.clearInteractionRefs();
root._hBrightness = brightnessIcon.parent;
root._hMic = micIcon.parent;
root._vAudio = audioIconV.parent;
root._vBrightness = brightnessIconV.parent;
root._vMic = micIconV.parent;
} }
Column { Column {
@@ -324,162 +446,151 @@ BasePill {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
Item { Repeater {
width: parent.width model: root.controlCenterRenderModel
height: root.vIconSize Item {
visible: root.showScreenSharingIcon && NiriService.hasCasts id: verticalGroupItem
required property var modelData
required property int index
property string interactionGroupId: modelData.id
DankIcon { width: parent.width
name: "screen_record" height: {
size: root.vIconSize switch (modelData.id) {
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText case "audio":
anchors.centerIn: parent return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0);
} case "microphone":
} return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0);
case "brightness":
return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0);
default:
return root.vIconSize;
}
}
visible: modelData.visible
Item { Component.onCompleted: {
width: parent.width root.registerInteractionDelegate(true, verticalGroupItem);
height: root.vIconSize root.refreshInteractionRefs();
visible: root.showNetworkIcon && NetworkService.networkAvailable }
Component.onDestruction: {
if (root) {
root.unregisterInteractionDelegate(verticalGroupItem);
root.refreshInteractionRefs();
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
DankIcon { DankIcon {
name: root.getNetworkIconName() anchors.centerIn: parent
size: root.vIconSize visible: !verticalGroupItem.modelData.composite
color: root.getNetworkIconColor() name: {
anchors.centerIn: parent switch (verticalGroupItem.modelData.id) {
} case "screenSharing":
} return "screen_record";
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.vIconSize
color: {
switch (verticalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
Item { DankIcon {
width: parent.width id: audioIconV
height: root.vIconSize visible: verticalGroupItem.modelData.id === "audio"
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected name: root.getVolumeIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
name: "vpn_lock" id: audioPercentV
size: root.vIconSize visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
anchors.centerIn: parent reserveText: "100%"
} font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
} color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
Item { DankIcon {
width: parent.width id: micIconV
height: root.vIconSize visible: verticalGroupItem.modelData.id === "microphone"
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled name: root.getMicIconName()
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
name: "bluetooth" id: micPercentV
size: root.vIconSize visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
anchors.centerIn: parent reserveText: "100%"
} font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
} color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
Item { DankIcon {
width: parent.width id: brightnessIconV
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0) visible: verticalGroupItem.modelData.id === "brightness"
visible: root.showAudioIcon name: root.getBrightnessIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
id: audioIconV id: brightnessPercentV
name: root.getVolumeIconName() visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness())
size: root.vIconSize text: Math.round(getBrightness() * 100) + "%"
color: Theme.widgetIconColor reserveText: "100%"
anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
anchors.top: parent.top color: Theme.widgetTextColor
} anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom
NumericText { anchors.topMargin: 2
id: audioPercentV }
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
visible: root.showMicIcon
DankIcon {
id: micIconV
name: root.getMicIconName()
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
NumericText {
id: micPercentV
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIconV
name: root.getBrightnessIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
NumericText {
id: brightnessPercentV
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showBatteryIcon && BatteryService.batteryAvailable
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: root.vIconSize
color: root.getBatteryIconColor()
anchors.centerIn: parent
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
DankIcon {
name: "print"
size: root.vIconSize
color: Theme.primary
anchors.centerIn: parent
} }
} }
@@ -503,157 +614,206 @@ BasePill {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { Repeater {
name: "screen_record" model: root.controlCenterRenderModel
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts
}
DankIcon { Item {
id: networkIcon id: horizontalGroupItem
name: root.getNetworkIconName() required property var modelData
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) required property int index
color: root.getNetworkIconColor() property string interactionGroupId: modelData.id
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon { width: {
id: vpnIcon switch (modelData.id) {
name: "vpn_lock" case "audio":
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) return audioGroup.width;
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText case "microphone":
anchors.verticalCenter: parent.verticalCenter return micGroup.width;
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected case "brightness":
} return brightnessGroup.width;
default:
return root.getControlCenterIconSize();
}
}
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2
visible: modelData.visible
DankIcon { Component.onCompleted: {
id: bluetoothIcon root.registerInteractionDelegate(false, horizontalGroupItem);
name: "bluetooth" root.refreshInteractionRefs();
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) }
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText Component.onDestruction: {
anchors.verticalCenter: parent.verticalCenter if (root) {
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled root.unregisterInteractionDelegate(horizontalGroupItem);
} root.refreshInteractionRefs();
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
Rectangle { DankIcon {
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4 id: iconOnlyItem
implicitWidth: width anchors.verticalCenter: parent.verticalCenter
height: root.widgetThickness - root.horizontalPadding * 2 anchors.left: parent.left
color: "transparent" visible: !horizontalGroupItem.modelData.composite
anchors.verticalCenter: parent.verticalCenter name: {
visible: root.showAudioIcon switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return "screen_record";
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.getControlCenterIconSize()
color: {
switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
DankIcon { Rectangle {
id: audioIcon id: audioGroup
name: root.getVolumeIconName() width: audioContent.implicitWidth + 2
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) implicitWidth: width
color: Theme.widgetIconColor height: parent.height
anchors.verticalCenter: parent.verticalCenter color: "transparent"
anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 2 visible: horizontalGroupItem.modelData.id === "audio"
Row {
id: audioContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: audioIcon
name: root.getVolumeIconName()
size: root.getControlCenterIconSize()
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: audioPercent
visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
Rectangle {
id: micGroup
width: micContent.implicitWidth + 2
implicitWidth: width
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "microphone"
Row {
id: micContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: micIcon
name: root.getMicIconName()
size: root.getControlCenterIconSize()
color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: micPercent
visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
Rectangle {
id: brightnessGroup
width: brightnessContent.implicitWidth + 2
implicitWidth: width
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "brightness"
Row {
id: brightnessContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: root.getControlCenterIconSize()
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: brightnessPercent
visible: root.showBrightnessPercent && isFinite(getBrightness())
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
} }
NumericText {
id: audioPercent
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: audioIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
Rectangle {
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.reservedWidth : 0) + 4
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showMicIcon
DankIcon {
id: micIcon
name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
}
NumericText {
id: micPercent
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: micIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
Rectangle {
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.reservedWidth : 0) + 4
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
}
NumericText {
id: brightnessPercent
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: brightnessIcon.right
anchors.leftMargin: 2
width: reservedWidth
}
}
DankIcon {
id: batteryIcon
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getBatteryIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable
}
DankIcon {
id: printerIcon
name: "print"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
} }
DankIcon { DankIcon {
name: "settings" name: "settings"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) size: root.getControlCenterIconSize()
color: root.isActive ? Theme.primary : Theme.widgetIconColor color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.hasNoVisibleIcons() visible: root.hasNoVisibleIcons()

View File

@@ -87,11 +87,11 @@ BasePill {
} }
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId); const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
return workspaceWindows.length > 0 && activeWindow && activeWindow.title; return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId);
} }
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) { if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) {
return false; return false;
} }
@@ -111,7 +111,7 @@ BasePill {
} }
} }
return activeWindow && activeWindow.title; return activeWindow && (activeWindow.title || activeWindow.appId);
} }
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0 width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
@@ -212,17 +212,19 @@ BasePill {
const title = activeWindow && activeWindow.title ? activeWindow.title : ""; const title = activeWindow && activeWindow.title ? activeWindow.title : "";
const appName = appText.text; const appName = appText.text;
if (compactMode && title === appName) { if (compactMode) {
if (!title || title === appName)
return title || appName;
if (title.endsWith(appName))
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "") || appName;
return title; return title;
} }
if (!title || !appName) { if (!title || !appName)
return title; return title;
}
if (title.endsWith(appName)) { if (title.endsWith(appName))
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, ""); return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
}
return title; return title;
} }

View File

@@ -12,7 +12,7 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
property int currentTabIndex: 0 property int currentTabIndex: 0
popupWidth: 700 popupWidth: SettingsData.showWeekNumber ? 736 : 700
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80 triggerWidth: 80
screen: triggerScreen screen: triggerScreen
@@ -168,6 +168,7 @@ DankPopout {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: Math.max(700, pages.implicitWidth + (Theme.spacingM * 2))
implicitHeight: contentColumn.height + Theme.spacingM * 2 implicitHeight: contentColumn.height + Theme.spacingM * 2
color: "transparent" color: "transparent"
focus: true focus: true
@@ -316,6 +317,7 @@ DankPopout {
id: pages id: pages
width: parent.width width: parent.width
height: implicitHeight height: implicitHeight
implicitWidth: currentItem && currentItem.implicitWidth > 0 ? currentItem.implicitWidth : (700 - Theme.spacingM * 2)
implicitHeight: { implicitHeight: {
if (root.currentTabIndex === 0) if (root.currentTabIndex === 0)
return overviewLoader.item?.implicitHeight ?? 410; return overviewLoader.item?.implicitHeight ?? 410;

View File

@@ -105,7 +105,7 @@ Item {
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2 implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
Connections { Connections {

View File

@@ -7,6 +7,8 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
property bool showEventDetails: false property bool showEventDetails: false
property date selectedDate: systemClock.date property date selectedDate: systemClock.date
property var selectedDateEvents: [] property var selectedDateEvents: []
@@ -41,6 +43,40 @@ Rectangle {
return d; return d;
} }
function getWeekNumber(dateObj) {
// Set time to noon to avoid potential Daylight Saving Time related bugs
const weekStartDay = startOfWeek(dateObj);
weekStartDay.setHours(12, 0, 0, 0);
let week1Start;
if (weekStartJs() === 1) {
// ISO 8601 Standard, week start on Monday
// A week belongs to the year its Thursday falls in
// So we have to get the yearTarget from weekStartDay instead of dateObj
let yearTarget = weekStartDay;
yearTarget.setDate(yearTarget.getDate() + 3); // Monday + 3 = Thursday
// Week 1 is the week containing Jan 4th
const jan4 = new Date(yearTarget.getFullYear(), 0, 4);
week1Start = startOfWeek(jan4);
} else {
// Traditional / US Standard, week start on Sunday
// A week belongs to the year its Sunday falls in
let yearTarget = weekStartDay;
yearTarget.setDate(yearTarget.getDate() + 6); // Monday + 6 = Sunday
// Week 1 is the week containing Jan 1st
const jan1 = new Date(yearTarget.getFullYear(), 0, 1);
week1Start = startOfWeek(jan1);
}
week1Start.setHours(12, 0, 0, 0);
const diffDays = Math.round((weekStartDay.getTime() - week1Start.getTime()) / 86400000); // Number of miliseconds in a day
return Math.floor(diffDays / 7) + 1;
}
function updateSelectedDateEvents() { function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) { if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate); const events = CalendarService.getEventsForDate(selectedDate);
@@ -151,6 +187,7 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
Row { Row {
width: parent.width width: parent.width
height: 28 height: 28
@@ -224,120 +261,172 @@ Rectangle {
Row { Row {
width: parent.width width: parent.width
height: 18 height: parent.height - 28 - Theme.spacingS
visible: !showEventDetails visible: !showEventDetails
spacing: SettingsData.showWeekNumber ? Theme.spacingS : 0
Repeater { Column {
model: { id: weekNumberColumn
const days = []; visible: SettingsData.showWeekNumber
const qtFirst = weekStartQt(); width: SettingsData.showWeekNumber ? 28 : 0
for (let i = 0; i < 7; ++i) { height: parent.height
const qtDay = ((qtFirst - 1 + i) % 7) + 1; spacing: Theme.spacingS
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
}
return days;
}
Rectangle { Item {
width: parent.width / 7 width: parent.width
height: 18 height: 18
color: "transparent" }
StyledText { Grid {
anchors.centerIn: parent width: parent.width
text: modelData height: parent.height - 18 - Theme.spacingS
font.pixelSize: Theme.fontSizeSmall columns: 1
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) rows: 6
font.weight: Font.Medium
Repeater {
model: 6
Rectangle {
width: parent.width
height: parent.height / 6
color: "transparent"
StyledText {
anchors.centerIn: parent
text: {
const rowDate = new Date(calendarGrid.firstDay);
rowDate.setDate(rowDate.getDate() + index * 7);
return root.getWeekNumber(rowDate);
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
} }
} }
} }
}
Grid { Column {
id: calendarGrid width: SettingsData.showWeekNumber ? (parent.width - weekNumberColumn.width - parent.spacing) : parent.width
visible: !showEventDetails height: parent.height
spacing: Theme.spacingS
property date displayDate: systemClock.date Row {
property date selectedDate: systemClock.date width: parent.width
height: 18
readonly property date firstDay: { Repeater {
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1); model: {
return startOfWeek(firstOfMonth); const days = [];
} const qtFirst = weekStartQt();
for (let i = 0; i < 7; ++i) {
width: parent.width const qtDay = ((qtFirst - 1 + i) % 7) + 1;
height: parent.height - 28 - 18 - Theme.spacingS * 2 days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
columns: 7 }
rows: 6 return days;
Repeater {
model: 42
Rectangle {
readonly property date dayDate: {
const date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday ? Font.Medium : Font.Normal
} }
Rectangle { Rectangle {
anchors.bottom: parent.bottom width: parent.width / 7
anchors.horizontalCenter: parent.horizontalCenter height: 18
anchors.bottomMargin: 4 color: "transparent"
width: 12
height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Behavior on opacity { StyledText {
NumberAnimation { anchors.centerIn: parent
duration: Theme.shortDuration text: modelData
easing.type: Theme.standardEasing font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
id: calendarGrid
width: parent.width
height: parent.height - 18 - Theme.spacingS
columns: 7
rows: 6
property date displayDate: systemClock.date
property date selectedDate: systemClock.date
readonly property date firstDay: {
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
return startOfWeek(firstOfMonth);
}
Repeater {
model: 42
Rectangle {
readonly property date dayDate: {
const date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday ? Font.Medium : Font.Normal
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 4
width: 12
height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate;
root.showEventDetails = true;
}
} }
} }
} }
} }
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate;
root.showEventDetails = true;
}
}
}
} }
} }
} }
DankListView { DankListView {
width: parent.width - Theme.spacingS * 2 width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS

View File

@@ -8,7 +8,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410 implicitHeight: 410
signal switchToWeatherTab signal switchToWeatherTab

View File

@@ -12,7 +12,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410 implicitHeight: 410
property string wallpaperDir: "" property string wallpaperDir: ""

View File

@@ -11,7 +11,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410 implicitHeight: 410
property bool syncing: false property bool syncing: false
property bool showHourly: false property bool showHourly: false

View File

@@ -0,0 +1,113 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
readonly property var muxTypeOptions: [
"tmux",
"zellij"
]
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
tab: "mux"
tags: ["mux", "multiplexer", "tmux", "zellij", "type"]
title: I18n.tr("Multiplexer")
iconName: "terminal"
SettingsDropdownRow {
tab: "mux"
tags: ["mux", "multiplexer", "tmux", "zellij", "type", "backend"]
settingKey: "muxType"
text: I18n.tr("Multiplexer Type")
description: I18n.tr("Terminal multiplexer backend to use")
options: root.muxTypeOptions
currentValue: SettingsData.muxType
onValueChanged: value => SettingsData.set("muxType", value)
}
}
SettingsCard {
tab: "mux"
tags: ["mux", "terminal", "custom", "command", "script"]
title: I18n.tr("Terminal")
iconName: "desktop_windows"
SettingsToggleRow {
tab: "mux"
tags: ["mux", "custom", "command", "override"]
settingKey: "muxUseCustomCommand"
text: I18n.tr("Use Custom Command")
description: I18n.tr("Override terminal with a custom command or script")
checked: SettingsData.muxUseCustomCommand
onToggled: checked => SettingsData.set("muxUseCustomCommand", checked)
}
Column {
width: parent?.width ?? 0
spacing: Theme.spacingS
visible: SettingsData.muxUseCustomCommand
StyledText {
width: parent.width
text: I18n.tr("The custom command used when attaching to sessions (receives the session name as the first argument)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
width: parent.width
text: SettingsData.muxCustomCommand
placeholderText: I18n.tr("Enter command or script path")
onTextEdited: SettingsData.set("muxCustomCommand", text)
}
}
}
SettingsCard {
tab: "mux"
tags: ["mux", "session", "filter", "exclude", "hide"]
title: I18n.tr("Session Filter")
iconName: "filter_list"
Column {
width: parent?.width ?? 0
spacing: Theme.spacingS
StyledText {
width: parent.width
text: I18n.tr("Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
width: parent.width
text: SettingsData.muxSessionFilter
placeholderText: I18n.tr("e.g., scratch, /^tmp_.*/, build")
onTextEdited: SettingsData.set("muxSessionFilter", text)
}
}
}
}
}
}

View File

@@ -85,6 +85,16 @@ Item {
settingKey: "dateFormat" settingKey: "dateFormat"
iconName: "calendar_today" iconName: "calendar_today"
SettingsToggleRow {
tab: "time"
tags: ["show", "week"]
settingKey: "showWeekNumber"
text: I18n.tr("Show Week Number")
description: I18n.tr("Show week number in the calendar")
checked: SettingsData.showWeekNumber
onToggled: checked => SettingsData.set("showWeekNumber", checked)
}
SettingsDropdownRow { SettingsDropdownRow {
tab: "time" tab: "time"
tags: ["first", "day", "week"] tags: ["first", "day", "week"]

View File

@@ -391,6 +391,7 @@ Item {
widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon; widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon;
widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon; widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon;
widgetObj.showScreenSharingIcon = SettingsData.controlCenterShowScreenSharingIcon; widgetObj.showScreenSharingIcon = SettingsData.controlCenterShowScreenSharingIcon;
widgetObj.controlCenterGroupOrder = ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"];
} }
if (widgetId === "runningApps") { if (widgetId === "runningApps") {
widgetObj.runningAppsCompactMode = SettingsData.runningAppsCompactMode; widgetObj.runningAppsCompactMode = SettingsData.runningAppsCompactMode;
@@ -429,7 +430,7 @@ Item {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -498,6 +499,32 @@ Item {
return; return;
var newWidget = cloneWidgetData(widgets[widgetIndex]); var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget[settingName] = value; newWidget[settingName] = value;
if (!value) {
switch (settingName) {
case "showAudioIcon":
newWidget.showAudioPercent = false;
break;
case "showMicIcon":
newWidget.showMicPercent = false;
break;
case "showBrightnessIcon":
newWidget.showBrightnessPercent = false;
break;
}
}
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleControlCenterGroupOrderChanged(sectionId, widgetIndex, groupOrder) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
var previousWidget = widgets[widgetIndex];
var newWidget = cloneWidgetData(previousWidget);
newWidget.controlCenterGroupOrder = groupOrder.slice();
widgets[widgetIndex] = newWidget; widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
} }
@@ -655,6 +682,8 @@ Item {
item.showPrinterIcon = widget.showPrinterIcon; item.showPrinterIcon = widget.showPrinterIcon;
if (widget.showScreenSharingIcon !== undefined) if (widget.showScreenSharingIcon !== undefined)
item.showScreenSharingIcon = widget.showScreenSharingIcon; item.showScreenSharingIcon = widget.showScreenSharingIcon;
if (widget.controlCenterGroupOrder !== undefined)
item.controlCenterGroupOrder = widget.controlCenterGroupOrder;
if (widget.minimumWidth !== undefined) if (widget.minimumWidth !== undefined)
item.minimumWidth = widget.minimumWidth; item.minimumWidth = widget.minimumWidth;
if (widget.showSwap !== undefined) if (widget.showSwap !== undefined)
@@ -948,6 +977,9 @@ Item {
onControlCenterSettingChanged: (sectionId, index, setting, value) => { onControlCenterSettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value);
} }
onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => {
widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder);
}
onPrivacySettingChanged: (sectionId, index, setting, value) => { onPrivacySettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value);
} }
@@ -1012,6 +1044,9 @@ Item {
onControlCenterSettingChanged: (sectionId, index, setting, value) => { onControlCenterSettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value);
} }
onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => {
widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder);
}
onPrivacySettingChanged: (sectionId, index, setting, value) => { onPrivacySettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value);
} }
@@ -1076,6 +1111,9 @@ Item {
onControlCenterSettingChanged: (sectionId, index, setting, value) => { onControlCenterSettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value); widgetsTab.handleControlCenterSettingChanged(sectionId, index, setting, value);
} }
onControlCenterGroupOrderChanged: (sectionId, index, groupOrder) => {
widgetsTab.handleControlCenterGroupOrderChanged(sectionId, index, groupOrder);
}
onPrivacySettingChanged: (sectionId, index, setting, value) => { onPrivacySettingChanged: (sectionId, index, setting, value) => {
widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value); widgetsTab.handlePrivacySettingChanged(sectionId, index, setting, value);
} }

View File

@@ -27,6 +27,7 @@ Column {
signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex)
signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath) signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath)
signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value) signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value)
signal controlCenterGroupOrderChanged(string sectionId, int widgetIndex, var groupOrder)
signal privacySettingChanged(string sectionId, int widgetIndex, string settingName, bool value) signal privacySettingChanged(string sectionId, int widgetIndex, string settingName, bool value)
signal minimumWidthChanged(string sectionId, int widgetIndex, bool enabled) signal minimumWidthChanged(string sectionId, int widgetIndex, bool enabled)
signal showSwapChanged(string sectionId, int widgetIndex, bool enabled) signal showSwapChanged(string sectionId, int widgetIndex, bool enabled)
@@ -39,7 +40,7 @@ Column {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -90,7 +91,6 @@ Column {
height: 70 height: 70
z: held ? 2 : 1 z: held ? 2 : 1
Rectangle { Rectangle {
id: itemBackground id: itemBackground
@@ -587,6 +587,7 @@ Column {
controlCenterContextMenu.widgetData = modelData; controlCenterContextMenu.widgetData = modelData;
controlCenterContextMenu.sectionId = root.sectionId; controlCenterContextMenu.sectionId = root.sectionId;
controlCenterContextMenu.widgetIndex = index; controlCenterContextMenu.widgetIndex = index;
controlCenterContextMenu.controlCenterGroups = controlCenterContextMenu.getOrderedControlCenterGroups();
var buttonPos = ccMenuButton.mapToItem(root, 0, 0); var buttonPos = ccMenuButton.mapToItem(root, 0, 0);
var popupWidth = controlCenterContextMenu.width; var popupWidth = controlCenterContextMenu.width;
@@ -1054,13 +1055,236 @@ Column {
property string sectionId: "" property string sectionId: ""
property int widgetIndex: -1 property int widgetIndex: -1
width: 220 readonly property real minimumContentWidth: controlCenterContentMetrics.implicitWidth + Theme.spacingS * 2
height: menuColumn.implicitHeight + Theme.spacingS * 2 readonly property real controlCenterRowHeight: 32
readonly property real controlCenterRowSpacing: 1
readonly property real controlCenterGroupVerticalPadding: Theme.spacingXS * 2
readonly property real controlCenterMenuSpacing: 2
width: Math.max(220, minimumContentWidth)
height: getControlCenterPopupHeight(controlCenterGroups)
padding: 0 padding: 0
modal: true modal: true
focus: true focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onClosed: {
cancelControlCenterDrag();
}
readonly property var defaultControlCenterGroups: [
{
id: "network",
rows: [
{
icon: "lan",
label: I18n.tr("Network"),
setting: "showNetworkIcon"
}
]
},
{
id: "vpn",
rows: [
{
icon: "vpn_lock",
label: I18n.tr("VPN"),
setting: "showVpnIcon"
}
]
},
{
id: "bluetooth",
rows: [
{
icon: "bluetooth",
label: I18n.tr("Bluetooth"),
setting: "showBluetoothIcon"
}
]
},
{
id: "audio",
rows: [
{
icon: "volume_up",
label: I18n.tr("Audio"),
setting: "showAudioIcon"
},
{
icon: "percent",
label: I18n.tr("Volume"),
setting: "showAudioPercent"
}
]
},
{
id: "microphone",
rows: [
{
icon: "mic",
label: I18n.tr("Microphone"),
setting: "showMicIcon"
},
{
icon: "percent",
label: I18n.tr("Microphone Volume"),
setting: "showMicPercent"
}
]
},
{
id: "brightness",
rows: [
{
icon: "brightness_high",
label: I18n.tr("Brightness"),
setting: "showBrightnessIcon"
},
{
icon: "percent",
label: I18n.tr("Brightness Value"),
setting: "showBrightnessPercent"
}
]
},
{
id: "battery",
rows: [
{
icon: "battery_full",
label: I18n.tr("Battery"),
setting: "showBatteryIcon"
}
]
},
{
id: "printer",
rows: [
{
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon"
}
]
},
{
id: "screenSharing",
rows: [
{
icon: "screen_record",
label: I18n.tr("Screen Sharing"),
setting: "showScreenSharingIcon"
}
]
}
]
property var controlCenterGroups: defaultControlCenterGroups
property int draggedControlCenterGroupIndex: -1
property int controlCenterGroupDropIndex: -1
function updateControlCenterGroupDropIndex(draggedIndex, localY) {
const totalGroups = controlCenterGroups.length;
let dropIndex = totalGroups;
for (let i = 0; i < totalGroups; i++) {
const delegate = groupRepeater.itemAt(i);
if (!delegate)
continue;
const midpoint = delegate.y + delegate.height / 2;
if (localY < midpoint) {
dropIndex = i;
break;
}
}
controlCenterGroupDropIndex = Math.max(0, Math.min(totalGroups, dropIndex));
draggedControlCenterGroupIndex = draggedIndex;
}
function finishControlCenterDrag() {
if (draggedControlCenterGroupIndex < 0) {
controlCenterGroupDropIndex = -1;
return;
}
const fromIndex = draggedControlCenterGroupIndex;
let toIndex = controlCenterGroupDropIndex;
draggedControlCenterGroupIndex = -1;
controlCenterGroupDropIndex = -1;
if (toIndex < 0 || toIndex > controlCenterGroups.length || toIndex === fromIndex || toIndex === fromIndex + 1)
return;
const groups = controlCenterGroups.slice();
const moved = groups.splice(fromIndex, 1)[0];
if (toIndex > fromIndex)
toIndex -= 1;
groups.splice(toIndex, 0, moved);
controlCenterGroups = groups;
const reorderedGroupIds = groups.map(group => group.id);
root.controlCenterGroupOrderChanged(sectionId, widgetIndex, reorderedGroupIds);
}
function cancelControlCenterDrag() {
draggedControlCenterGroupIndex = -1;
controlCenterGroupDropIndex = -1;
}
function getControlCenterGroupHeight(group) {
const rowCount = group?.rows?.length ?? 0;
if (rowCount <= 0)
return controlCenterGroupVerticalPadding;
return rowCount * controlCenterRowHeight + Math.max(0, rowCount - 1) * controlCenterRowSpacing + controlCenterGroupVerticalPadding;
}
function getControlCenterPopupHeight(groups) {
const orderedGroups = groups || [];
let totalHeight = Theme.spacingS * 2;
for (let i = 0; i < orderedGroups.length; i++) {
totalHeight += getControlCenterGroupHeight(orderedGroups[i]);
if (i < orderedGroups.length - 1)
totalHeight += controlCenterMenuSpacing;
}
return totalHeight;
}
function getOrderedControlCenterGroups() {
const baseGroups = defaultControlCenterGroups.slice();
const currentWidget = contentItem.getCurrentWidgetData();
const savedOrder = currentWidget?.controlCenterGroupOrder;
if (!savedOrder || !savedOrder.length)
return baseGroups;
const groupMap = {};
for (let i = 0; i < baseGroups.length; i++)
groupMap[baseGroups[i].id] = baseGroups[i];
const orderedGroups = [];
for (let i = 0; i < savedOrder.length; i++) {
const groupId = savedOrder[i];
const group = groupMap[groupId];
if (group) {
orderedGroups.push(group);
delete groupMap[groupId];
}
}
for (let i = 0; i < baseGroups.length; i++) {
const group = baseGroups[i];
if (groupMap[group.id])
orderedGroups.push(group);
}
return orderedGroups;
}
background: Rectangle { background: Rectangle {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -1069,83 +1293,64 @@ Column {
} }
contentItem: Item { contentItem: Item {
function getCurrentWidgetData() {
const widgets = root.items || [];
if (controlCenterContextMenu.widgetIndex >= 0 && controlCenterContextMenu.widgetIndex < widgets.length)
return widgets[controlCenterContextMenu.widgetIndex];
return controlCenterContextMenu.widgetData;
}
Column { Column {
id: menuColumn id: menuColumn
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
spacing: 2 spacing: 2
Repeater { Item {
model: [ id: controlCenterContentMetrics
{ visible: false
icon: "lan", implicitWidth: 16 + Theme.spacingS + 16 + Theme.spacingS + longestControlCenterLabelMetrics.advanceWidth + Theme.spacingM + 40 + Theme.spacingS * 2 + Theme.spacingM
label: I18n.tr("Network"), }
setting: "showNetworkIcon"
}, TextMetrics {
{ id: longestControlCenterLabelMetrics
icon: "vpn_lock", font.pixelSize: Theme.fontSizeSmall
label: I18n.tr("VPN"), text: {
setting: "showVpnIcon" const labels = [
}, I18n.tr("Network"),
{ I18n.tr("VPN"),
icon: "bluetooth", I18n.tr("Bluetooth"),
label: I18n.tr("Bluetooth"), I18n.tr("Audio"),
setting: "showBluetoothIcon" I18n.tr("Volume"),
}, I18n.tr("Microphone"),
{ I18n.tr("Microphone Volume"),
icon: "volume_up", I18n.tr("Brightness"),
label: I18n.tr("Audio"), I18n.tr("Brightness Value"),
setting: "showAudioIcon" I18n.tr("Battery"),
}, I18n.tr("Printer"),
{ I18n.tr("Screen Sharing")
icon: "percent", ];
label: I18n.tr("Volume"), let longest = "";
setting: "showAudioPercent" for (let i = 0; i < labels.length; i++) {
}, if (labels[i].length > longest.length)
{ longest = labels[i];
icon: "mic", }
label: I18n.tr("Microphone"), return longest;
setting: "showMicIcon" }
}, }
{
icon: "percent", Repeater {
label: I18n.tr("Microphone Volume"), model: controlCenterContextMenu.controlCenterGroups
setting: "showMicPercent"
}, delegate: Item {
{ id: delegateRoot
icon: "brightness_high",
label: I18n.tr("Brightness"),
setting: "showBrightnessIcon"
},
{
icon: "percent",
label: I18n.tr("Brightness Value"),
setting: "showBrightnessPercent"
},
{
icon: "battery_full",
label: I18n.tr("Battery"),
setting: "showBatteryIcon"
},
{
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon"
},
{
icon: "screen_record",
label: I18n.tr("Screen Sharing"),
setting: "showScreenSharingIcon"
}
]
delegate: Rectangle {
required property var modelData required property var modelData
required property int index required property int index
function getCheckedState() { function getCheckedState(settingName) {
var wd = controlCenterContextMenu.widgetData; const wd = controlCenterContextMenu.contentItem.getCurrentWidgetData();
switch (modelData.setting) { switch (settingName) {
case "showNetworkIcon": case "showNetworkIcon":
return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon; return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
case "showVpnIcon": case "showVpnIcon":
@@ -1175,57 +1380,197 @@ Column {
} }
} }
readonly property string rootSetting: modelData.rows[0]?.setting ?? ""
readonly property bool rootEnabled: rootSetting ? getCheckedState(rootSetting) : true
readonly property bool isDragged: controlCenterContextMenu.draggedControlCenterGroupIndex === index
readonly property bool showDropIndicatorAbove: controlCenterContextMenu.controlCenterGroupDropIndex === index
readonly property bool showDropIndicatorBelow: controlCenterContextMenu.controlCenterGroupDropIndex === controlCenterContextMenu.controlCenterGroups.length && index === controlCenterContextMenu.controlCenterGroups.length - 1
width: menuColumn.width width: menuColumn.width
height: 32 height: groupBackground.height
radius: Theme.cornerRadius
color: toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row { Rectangle {
id: groupBackground
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: modelData.icon
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: toggle
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingS anchors.top: parent.top
anchors.verticalCenter: parent.verticalCenter height: groupContent.implicitHeight + Theme.spacingXS * 2
width: 40 radius: Theme.cornerRadius
height: 20 color: isDragged ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.18) : (groupHoverArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent")
checked: getCheckedState() opacity: isDragged ? 0.75 : 1.0
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled);
}
} }
MouseArea { Rectangle {
id: toggleArea anchors.left: parent.left
anchors.fill: parent anchors.right: parent.right
hoverEnabled: true anchors.top: parent.top
cursorShape: Qt.PointingHandCursor anchors.topMargin: -1
onPressed: { height: 2
toggle.checked = !toggle.checked; radius: 1
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggle.checked); color: Theme.primary
visible: showDropIndicatorAbove
z: 3
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: -1
height: 2
radius: 1
color: Theme.primary
visible: showDropIndicatorBelow
z: 3
}
Item {
id: groupContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: Theme.spacingXS
implicitHeight: groupColumn.implicitHeight
Column {
id: groupColumn
anchors.left: parent.left
anchors.right: parent.right
spacing: 1
Repeater {
id: groupColumnRepeater
model: modelData.rows
delegate: Rectangle {
required property var modelData
required property int index
readonly property var rowData: modelData
readonly property bool isFirstRow: index === 0
readonly property bool rowEnabled: isFirstRow ? true : delegateRoot.rootEnabled
readonly property bool computedCheckedState: rowEnabled ? getCheckedState(rowData.setting) : false
readonly property bool rowHovered: rowEnabled && (toggleArea.containsMouse || (isFirstRow && groupDragHandleArea.containsMouse))
width: groupColumn.width
height: 32
radius: Theme.cornerRadius
opacity: rowEnabled ? 1.0 : 0.5
color: rowHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: toggle.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 16
color: groupDragHandleArea.pressed || isDragged ? Theme.primary : Theme.outline
visible: isFirstRow
}
MouseArea {
id: groupDragHandleArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
enabled: isFirstRow
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: mouse => {
mouse.accepted = true;
const point = mapToItem(menuColumn, mouse.x, mouse.y);
controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y);
}
onPositionChanged: mouse => {
if (!pressed)
return;
mouse.accepted = true;
const point = mapToItem(menuColumn, mouse.x, mouse.y);
controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y);
}
onReleased: mouse => {
mouse.accepted = true;
const point = mapToItem(menuColumn, mouse.x, mouse.y);
controlCenterContextMenu.updateControlCenterGroupDropIndex(delegateRoot.index, point.y);
controlCenterContextMenu.finishControlCenterDrag();
}
onCanceled: {
controlCenterContextMenu.cancelControlCenterDrag();
}
}
}
DankIcon {
name: rowData.icon
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: rowData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: toggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
enabled: rowEnabled
checked: computedCheckedState
onToggled: {
if (!rowEnabled)
return;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, toggled);
}
}
MouseArea {
id: toggleArea
anchors.fill: parent
anchors.leftMargin: 16 + Theme.spacingS * 2
hoverEnabled: true
cursorShape: rowEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: rowEnabled && controlCenterContextMenu.draggedControlCenterGroupIndex < 0
onPressed: {
if (!rowEnabled)
return;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, rowData.setting, !computedCheckedState);
}
}
}
}
}
MouseArea {
id: groupHoverArea
anchors.fill: parent
hoverEnabled: true
enabled: false
} }
} }
} }
id: groupRepeater
} }
} }
} }

View File

@@ -0,0 +1,230 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property var sessions: []
property bool loading: false
property bool tmuxAvailable: false
property bool zellijAvailable: false
readonly property bool currentMuxAvailable: muxType === "zellij" ? zellijAvailable : tmuxAvailable
readonly property string muxType: SettingsData.muxType
readonly property string displayName: muxType === "zellij" ? "Zellij" : "Tmux"
readonly property var terminalFlags: ({
"ghostty": ["-e"],
"kitty": ["-e"],
"alacritty": ["-e"],
"foot": [],
"wezterm": ["start", "--"],
"gnome-terminal": ["--"],
"xterm": ["-e"],
"konsole": ["-e"],
"st": ["-e"],
"terminator": ["-e"],
"xfce4-terminal": ["-e"]
})
function getTerminalFlag(terminal) {
return terminalFlags[terminal] ?? ["-e"]
}
readonly property string terminal: Quickshell.env("TERMINAL") || "ghostty"
function _terminalPrefix() {
return [terminal].concat(getTerminalFlag(terminal))
}
Process {
id: tmuxCheckProcess
command: ["which", "tmux"]
running: false
onExited: (code) => { root.tmuxAvailable = (code === 0) }
}
Process {
id: zellijCheckProcess
command: ["which", "zellij"]
running: false
onExited: (code) => { root.zellijAvailable = (code === 0) }
}
function checkAvailability() {
tmuxCheckProcess.running = true
zellijCheckProcess.running = true
}
Component.onCompleted: checkAvailability()
Process {
id: listProcess
running: false
stdout: StdioCollector {
onStreamFinished: {
try {
if (root.muxType === "zellij")
root._parseZellijSessions(text)
else
root._parseTmuxSessions(text)
} catch (e) {
console.error("[MuxService] Error parsing sessions:", e)
root.sessions = []
}
root.loading = false
}
}
stderr: SplitParser {
onRead: (line) => {
if (line.trim())
console.error("[MuxService] stderr:", line)
}
}
onExited: (code) => {
if (code !== 0 && code !== 1) {
console.warn("[MuxService] Process exited with code:", code)
root.sessions = []
}
root.loading = false
}
}
function refreshSessions() {
if (!root.currentMuxAvailable) {
root.sessions = []
return
}
root.loading = true
if (listProcess.running)
listProcess.running = false
if (root.muxType === "zellij")
listProcess.command = ["zellij", "list-sessions", "--no-formatting"]
else
listProcess.command = ["tmux", "list-sessions", "-F", "#{session_name}|#{session_windows}|#{session_attached}"]
Qt.callLater(function () {
listProcess.running = true
})
}
function _isSessionExcluded(name) {
var filter = SettingsData.muxSessionFilter.trim()
if (filter.length === 0)
return false
var parts = filter.split(",")
for (var i = 0; i < parts.length; i++) {
var pattern = parts[i].trim()
if (pattern.length === 0)
continue
if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
try {
var re = new RegExp(pattern.slice(1, -1))
if (re.test(name))
return true
} catch (e) {}
} else {
if (name.toLowerCase() === pattern.toLowerCase())
return true
}
}
return false
}
function _parseTmuxSessions(output) {
var sessionList = []
var lines = output.trim().split('\n')
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (line.length === 0)
continue
var parts = line.split('|')
if (parts.length >= 3 && !_isSessionExcluded(parts[0])) {
sessionList.push({
name: parts[0],
windows: parts[1],
attached: parts[2] === "1"
})
}
}
root.sessions = sessionList
}
function _parseZellijSessions(output) {
var sessionList = []
var lines = output.trim().split('\n')
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (line.length === 0)
continue
var exited = line.includes("(EXITED")
var bracketIdx = line.indexOf(" [")
var name = (bracketIdx > 0 ? line.substring(0, bracketIdx) : line).trim()
if (!_isSessionExcluded(name)) {
sessionList.push({
name: name,
windows: "N/A",
attached: !exited
})
}
}
root.sessions = sessionList
}
function attachToSession(name) {
if (SettingsData.muxUseCustomCommand && SettingsData.muxCustomCommand) {
Quickshell.execDetached([SettingsData.muxCustomCommand, name])
} else if (root.muxType === "zellij") {
Quickshell.execDetached(_terminalPrefix().concat(["zellij", "attach", name]))
} else {
Quickshell.execDetached(_terminalPrefix().concat(["tmux", "attach", "-t", name]))
}
}
function createSession(name) {
if (SettingsData.muxUseCustomCommand && SettingsData.muxCustomCommand) {
Quickshell.execDetached([SettingsData.muxCustomCommand, name])
} else if (root.muxType === "zellij") {
Quickshell.execDetached(_terminalPrefix().concat(["zellij", "-s", name]))
} else {
Quickshell.execDetached(_terminalPrefix().concat(["tmux", "new-session", "-s", name]))
}
}
readonly property bool supportsRename: muxType !== "zellij"
function renameSession(oldName, newName) {
if (root.muxType === "zellij")
return
Quickshell.execDetached(["tmux", "rename-session", "-t", oldName, newName])
Qt.callLater(refreshSessions)
}
function killSession(name) {
if (root.muxType === "zellij") {
Quickshell.execDetached(["zellij", "kill-session", name])
} else {
Quickshell.execDetached(["tmux", "kill-session", "-t", name])
}
Qt.callLater(refreshSessions)
}
}

View File

@@ -453,8 +453,8 @@ Item {
visible: false visible: false
x: contentContainer.x x: contentContainer.x
y: contentContainer.y y: contentContainer.y
width: root.alignedWidth width: shouldBeVisible ? root.alignedWidth : 0
height: root.alignedHeight height: shouldBeVisible ? root.alignedHeight : 0
} }
MouseArea { MouseArea {

View File

@@ -162,6 +162,13 @@ StyledRect {
if (root.keyForwardTargets[i]) if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event); root.keyForwardTargets[i].Keys.pressed(event);
} }
return;
}
if ((event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) && root.keyForwardTargets.length > 0) {
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event);
}
} }
} }

View File

@@ -99,8 +99,8 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius radius: Theme.cornerRadius
// M3 disabled track: on surface 12% opacity // Distinguish disabled checked vs unchecked so unchecked disabled switches don't look enabled
color: !toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha) color: !toggle.enabled ? (toggle.checked ? Qt.alpha(Theme.surfaceText, 0.12) : "transparent") : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha)
opacity: toggle.toggling ? 0.6 : 1 opacity: toggle.toggling ? 0.6 : 1
// M3 disabled unchecked border: on surface 12% opacity // M3 disabled unchecked border: on surface 12% opacity
@@ -119,8 +119,8 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// M3 disabled thumb: // M3 disabled thumb:
// checked = solid surface | unchecked = on surface 38% // checked = solid surface | unchecked = outlined off-state thumb
color: !toggle.enabled ? (toggle.checked ? Theme.surface : Qt.alpha(Theme.surfaceText, 0.38)) : (toggle.checked ? Theme.surface : Theme.outline) color: !toggle.enabled ? (toggle.checked ? Theme.surface : "transparent") : (toggle.checked ? Theme.surface : Theme.outline)
border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
border.width: (toggle.checked && toggle.enabled) ? 1 : 2 border.width: (toggle.checked && toggle.enabled) ? 1 : 2
@@ -165,8 +165,8 @@ Item {
// M3 disabled icon: on surface 38% // M3 disabled icon: on surface 38%
color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38) color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
filled: true filled: true
opacity: toggle.checked ? 1 : 0 opacity: (toggle.checked && toggle.enabled) ? 1 : 0
scale: toggle.checked ? 1 : 0.6 scale: (toggle.checked && toggle.enabled) ? 1 : 0.6
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff