diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/modules/common/Appearance.qml index 8929e348f..e9635fbeb 100644 --- a/.config/quickshell/modules/common/Appearance.qml +++ b/.config/quickshell/modules/common/Appearance.qml @@ -120,9 +120,11 @@ Singleton { property color colLayer2Active: mix(colLayer2, colOnLayer2, 0.80); property color colLayer3Hover: mix(colLayer3, colOnLayer3, 0.90); property color colLayer3Active: mix(colLayer3, colOnLayer3, 0.80); - property color colPrimaryHover: mix(m3colors.m3primary, colLayer1Hover, 0.7) - property color colPrimaryActive: mix(m3colors.m3primary, colLayer1Active, 0.4) - property color colSecondaryHover: mix(m3colors.m3secondary, colLayer1Hover, 0.7) + property color colPrimaryHover: mix(m3colors.m3primary, colLayer1Hover, 0.85) + property color colPrimaryActive: mix(m3colors.m3primary, colLayer1Active, 0.7) + property color colPrimaryContainerHover: mix(m3colors.m3primaryContainer, colLayer1Hover, 0.7) + property color colPrimaryContainerActive: mix(m3colors.m3primaryContainer, colLayer1Active, 0.6) + property color colSecondaryHover: mix(m3colors.m3secondary, colLayer1Hover, 0.85) property color colSecondaryActive: mix(m3colors.m3secondary, colLayer1Active, 0.4) property color colSecondaryContainerHover: mix(m3colors.m3secondaryContainer, colLayer1Hover, 0.8) property color colSecondaryContainerActive: mix(m3colors.m3secondaryContainer, colLayer1Active, 0.6) @@ -144,7 +146,7 @@ Singleton { property QtObject family: QtObject { property string main: "Rubik" property string title: "Rubik" - property string iconMaterial: "Material Symbols Rounded" + property string iconMaterial: "Material Symbols Outlined" property string iconNerd: "SpaceMono NF" property string monospace: "JetBrains Mono NF" property string reading: "Readex Pro" @@ -189,6 +191,8 @@ Singleton { property int sidebarWidth: 450 property int hyprlandGapsOut: 5 property int elevationMargin: 7 + property int fabShadowRadius: 5 + property int fabHoveredShadowRadius: 7 } } diff --git a/.config/quickshell/modules/common/widgets/DialogButton.qml b/.config/quickshell/modules/common/widgets/DialogButton.qml new file mode 100644 index 000000000..46dfa16d8 --- /dev/null +++ b/.config/quickshell/modules/common/widgets/DialogButton.qml @@ -0,0 +1,48 @@ +import "root:/modules/common" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io + +Button { + id: button + + property string buttonText + implicitHeight: 30 + implicitWidth: buttonTextWidget.implicitWidth + 15 * 2 + + background: Rectangle { + anchors.fill: parent + radius: Appearance.rounding.full + color: (button.down && button.enabled) ? Appearance.colors.colLayer1Active : ((button.hovered && button.enabled) ? Appearance.colors.colLayer1Hover : Appearance.transparentize(Appearance.m3colors.m3surfaceContainerHigh, 1)) + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } + + } + + } + + contentItem: StyledText { + id: buttonTextWidget + anchors.fill: parent + anchors.leftMargin: 15 + anchors.rightMargin: 15 + text: buttonText + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.normal + color: button.enabled ? Appearance.m3colors.m3primary : Appearance.m3colors.m3outline + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } + } + } + +} diff --git a/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml b/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml index 162077f53..8479c2f49 100644 --- a/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml +++ b/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml @@ -21,21 +21,16 @@ Rectangle { {"type": "calendar", "name": "Calendar", "icon": "calendar_month", "widget": calendarWidget}, {"type": "todo", "name": "To Do", "icon": "done_outline", "widget": todoWidget} ] - // Calendar - Component { - id: calendarWidget - CalendarWidget { - anchors.centerIn: parent - } - } - - // To Do - Component { - id: todoWidget - TodoWidget { - anchors.fill: parent - anchors.margins: 5 + Keys.onPressed: (event) => { + if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) + && event.modifiers === Qt.ControlModifier) { + if (event.key === Qt.Key_PageDown) { + root.selectedTab = Math.min(root.selectedTab + 1, root.tabs.length - 1) + } else if (event.key === Qt.Key_PageUp) { + root.selectedTab = Math.max(root.selectedTab - 1, 0) + } + event.accepted = true; } } @@ -72,7 +67,7 @@ Rectangle { height: tabStack.children[0]?.tabLoader?.implicitHeight // TODO: make this less stupid Layout.alignment: Qt.AlignVCenter property int realIndex: 0 - property int animationDuration: Appearance.animation.elementDecel.duration * 1.5 + property int animationDuration: Appearance.animation.elementDecelFast.duration * 1.5 // Switch the tab on halfway of the anim duration Connections { @@ -109,9 +104,28 @@ Rectangle { id: tabLoader anchors.fill: parent sourceComponent: modelData.widget + focus: root.selectedTab === tabItem.tabIndex } } } } } + + // Calendar component + Component { + id: calendarWidget + + CalendarWidget { + anchors.centerIn: parent + } + } + + // To Do component + Component { + id: todoWidget + TodoWidget { + anchors.fill: parent + anchors.margins: 5 + } + } } \ No newline at end of file diff --git a/.config/quickshell/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/modules/sidebarRight/SidebarRight.qml index 92ebf0447..086dfb33e 100644 --- a/.config/quickshell/modules/sidebarRight/SidebarRight.qml +++ b/.config/quickshell/modules/sidebarRight/SidebarRight.qml @@ -76,7 +76,7 @@ Scope { radius: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1 focus: true - Keys.onPressed: { + Keys.onPressed: (event) => { if (event.key === Qt.Key_Escape) { sidebarRoot.visible = false; event.accepted = true; // Prevent further propagation of the event diff --git a/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml b/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml index 8da0e1e07..13eb59182 100644 --- a/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml +++ b/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml @@ -14,9 +14,20 @@ Item { width: calendarColumn.width implicitHeight: calendarColumn.height + 10 * 2 + Keys.onPressed: (event) => { + if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) + && event.modifiers === Qt.NoModifier) { + if (event.key === Qt.Key_PageDown) { + monthShift++; + } else if (event.key === Qt.Key_PageUp) { + monthShift--; + } + event.accepted = true; + } + } MouseArea { anchors.fill: parent - onWheel: { + onWheel: (event) => { if (wheel.angleDelta.y > 0) { monthShift--; } else if (wheel.angleDelta.y < 0) { @@ -24,6 +35,7 @@ Item { } } } + ColumnLayout { id: calendarColumn anchors.centerIn: parent diff --git a/.config/quickshell/modules/sidebarRight/todo/TaskList.qml b/.config/quickshell/modules/sidebarRight/todo/TaskList.qml index 25cb0e218..16e773465 100644 --- a/.config/quickshell/modules/sidebarRight/todo/TaskList.qml +++ b/.config/quickshell/modules/sidebarRight/todo/TaskList.qml @@ -8,10 +8,14 @@ import QtQuick.Layouts Item { id: root required property var taskList; + property string emptyPlaceholderIcon + property string emptyPlaceholderText property int todoListItemSpacing: 5 property int todoListItemPadding: 8 + property int listBottomPadding: 80 Flickable { // Scrolled window + id: flickable anchors.fill: parent contentHeight: columnLayout.height clip: true @@ -123,6 +127,35 @@ Item { } } } + + } + // Bottom padding + Item { + implicitHeight: listBottomPadding + } + } + } + // Placeholder when list is empty + Item { + visible: taskList.length === 0 + anchors.fill: parent + + ColumnLayout { + anchors.centerIn: parent + spacing: 5 + + MaterialSymbol { + Layout.alignment: Qt.AlignHCenter + text: emptyPlaceholderIcon + font.pixelSize: 55 + color: Appearance.m3colors.m3outline + } + StyledText { + Layout.alignment: Qt.AlignHCenter + text: emptyPlaceholderText + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.m3colors.m3outline + horizontalAlignment: Text.AlignHCenter } } } diff --git a/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml b/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml index f7ec48ee1..539db74eb 100644 --- a/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml +++ b/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml @@ -4,9 +4,37 @@ import "root:/services" import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt5Compat.GraphicalEffects Item { + id: root property int currentTab: 0 + property var tabButtonList: [{"icon": "checklist", "name": "Unfinished"}, {"name": "Done", "icon": "check_circle"}] + property bool showAddDialog: false + property int dialogMargins: 25 + property int fabSize: 48 + property int fabMargins: 14 + + Keys.onPressed: (event) => { + if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) { + if (event.key === Qt.Key_PageDown) { + currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1) + } else if (event.key === Qt.Key_PageUp) { + currentTab = Math.max(currentTab - 1, 0) + } + event.accepted = true; + } + // Open add dialog on "N" (any modifiers) + else if (event.key === Qt.Key_N) { + root.showAddDialog = true + event.accepted = true; + } + // Close dialog on Esc if open + else if (event.key === Qt.Key_Escape && root.showAddDialog) { + root.showAddDialog = false + event.accepted = true; + } + } ColumnLayout { anchors.fill: parent @@ -14,7 +42,6 @@ Item { TabBar { id: tabBar - property var tabButtonList: [{"icon": "checklist", "name": "Unfinished"}, {"name": "Done", "icon": "check_circle"}] Layout.fillWidth: true currentIndex: currentTab onCurrentIndexChanged: currentTab = currentIndex @@ -23,7 +50,7 @@ Item { WheelHandler { onWheel: (event) => { if (event.angleDelta.y < 0) - currentTab = Math.min(currentTab + 1, tabBar.tabButtonList.length - 1) + currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1) else if (event.angleDelta.y > 0) currentTab = Math.max(currentTab - 1, 0) } @@ -32,7 +59,7 @@ Item { } Repeater { - model: tabBar.tabButtonList + model: root.tabButtonList delegate: StyledTabButton { selected: (index == currentTab) buttonText: modelData.name @@ -50,8 +77,8 @@ Item { height: 3 radius: Appearance.rounding.full - width: tabBar.width / tabBar.tabButtonList.length - indicatorPadding * 2 - x: indicatorPadding + tabBar.width / tabBar.tabButtonList.length * currentTab + width: tabBar.width / root.tabButtonList.length - indicatorPadding * 2 + x: indicatorPadding + tabBar.width / root.tabButtonList.length * currentTab z: 2 Behavior on x { SmoothedAnimation { velocity: Appearance.animation.positionShift.velocity @@ -71,11 +98,17 @@ Item { // To Do tab TaskList { + listBottomPadding: root.fabSize + root.fabMargins * 2 + emptyPlaceholderIcon: "check_circle" + emptyPlaceholderText: "Nothing here!" taskList: Todo.list .map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); }) .filter(function(item) { return !item.done; }) } TaskList { + listBottomPadding: root.fabSize + root.fabMargins * 2 + emptyPlaceholderIcon: "checklist" + emptyPlaceholderText: "Finished tasks will go here" taskList: Todo.list .map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); }) .filter(function(item) { return item.done; }) @@ -83,4 +116,164 @@ Item { } } + + // + FAB + Button { + id: fabButton + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: root.fabMargins + anchors.bottomMargin: root.fabMargins + + width: root.fabSize + height: root.fabSize + + onClicked: root.showAddDialog = true + + background: Rectangle { + id: fabBackground + anchors.fill: parent + radius: Appearance.rounding.small + color: (fabButton.down) ? Appearance.colors.colPrimaryContainerActive : (fabButton.hovered ? Appearance.colors.colPrimaryContainerHover : Appearance.m3colors.m3primaryContainer) + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } + + } + + } + + DropShadow { + id: fabShadow + anchors.fill: fabBackground + source: fabBackground + horizontalOffset: 0 + verticalOffset: fabButton.hovered ? 4 : 2 + radius: fabButton.hovered ? Appearance.sizes.fabHoveredShadowRadius : Appearance.sizes.fabShadowRadius + samples: fabShadow.radius * 2 + 1 + color: Appearance.transparentize(Appearance.m3colors.m3shadow, 0.55) + z: fabBackground.z - 1 + + Behavior on verticalOffset { + NumberAnimation { + duration: Appearance.animation.elementDecelFast.duration + easing.type: Appearance.animation.elementDecelFast.type + } + } + } + + contentItem: MaterialSymbol { + text: "add" + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.huge + color: Appearance.m3colors.m3onPrimaryContainer + } + } + + Item { + anchors.fill: parent + visible: root.showAddDialog + z: 1000 + + onVisibleChanged: { + if (!visible) { + todoInput.text = "" + fabButton.focus = true + } + } + + Rectangle { // Scrim + anchors.fill: parent + radius: Appearance.rounding.small + color: "#80000000" + MouseArea { + hoverEnabled: true + anchors.fill: parent + preventStealing: true + propagateComposedEvents: false + } + } + + Rectangle { // The dialog + id: dialog + implicitWidth: parent.width - dialogMargins * 2 + implicitHeight: dialogColumnLayout.implicitHeight + anchors.centerIn: parent + color: Appearance.m3colors.m3surfaceContainerHigh + radius: Appearance.rounding.normal + + function addTask() { + if (todoInput.text.length > 0) { + Todo.addTask(todoInput.text) + todoInput.text = "" + root.showAddDialog = false + root.currentTab = 0 // Show unfinished tasks + } + } + + ColumnLayout { + anchors.fill: parent + id: dialogColumnLayout + spacing: 16 + + StyledText { + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.alignment: Qt.AlignLeft + color: Appearance.m3colors.m3onSurface + font.pixelSize: Appearance.font.pixelSize.larger + text: "Add task" + } + + TextField { + id: todoInput + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + padding: 10 + color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant + selectedTextColor: Appearance.m3colors.m3onSurface + placeholderText: "Task description" + focus: root.showAddDialog + onAccepted: dialog.addTask() + + background: Rectangle { + anchors.fill: parent + radius: 8 + border.width: 2 + border.color: todoInput.activeFocus ? Appearance.m3colors.m3primary : Appearance.m3colors.m3outline + color: "transparent" + } + + cursorDelegate: Rectangle { + width: 1 + color: todoInput.activeFocus ? Appearance.m3colors.m3primary : "transparent" + radius: 1 + } + } + + RowLayout { + Layout.bottomMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.alignment: Qt.AlignRight + spacing: 5 + + DialogButton { + buttonText: "Cancel" + onClicked: root.showAddDialog = false + } + DialogButton { + buttonText: "Add" + enabled: todoInput.text.length > 0 + onClicked: dialog.addTask() + } + } + } + } + } } \ No newline at end of file diff --git a/.config/quickshell/services/Todo.qml b/.config/quickshell/services/Todo.qml index 96b90de95..0dc062871 100644 --- a/.config/quickshell/services/Todo.qml +++ b/.config/quickshell/services/Todo.qml @@ -13,9 +13,19 @@ Singleton { function addItem(item) { list.push(item) + // Reassign to trigger onListChanged + root.list = list.slice(0) todoFileView.setText(JSON.stringify(root.list)) } + function addTask(desc) { + const item = { + "content": desc, + "done": false, + } + addItem(item) + } + function markDone(index) { if (index >= 0 && index < list.length) { list[index].done = true