Launcher: remove redundant NIcopnTabButton

This commit is contained in:
Lysec
2026-02-14 02:35:57 +01:00
parent 63ca0aabb7
commit 38b616ba0a
6 changed files with 324 additions and 107 deletions
+1 -1
View File
@@ -1049,7 +1049,7 @@
"keybinds-left": "Move left",
"keybinds-modifier-description": "Shortcuts must include a modifier key (Ctrl or Alt).",
"keybinds-modifier-title": "Modifier required",
"keybinds-remove": "Remove/Delete",
"keybinds-remove": "Remove / Delete",
"keybinds-right": "Move right",
"keybinds-title": "Navigation keybinds",
"keybinds-up": "Move up",
+1 -1
View File
@@ -749,7 +749,7 @@ Rectangle {
Repeater {
model: root.providerCategories
NIconTabButton {
NTabButton {
required property string modelData
required property int index
icon: root.currentProvider.categoryIcons ? (root.currentProvider.categoryIcons[modelData] || "star") : "star"
@@ -4,6 +4,7 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import Quickshell.Wayland
import "../../../Helpers/Keybinds.js" as Keybinds
import qs.Commons
import qs.Modules.MainScreen
import qs.Services.System
@@ -23,6 +24,215 @@ SmartPanel {
panelContent: Rectangle {
id: panelContent
color: "transparent"
focus: true
// Force focus when opened
Connections {
target: root
function onOpened() {
panelContent.forceActiveFocus();
}
}
Keys.onPressed: event => {
// Tab navigation for categories
if (event.key === Qt.Key_Tab) {
currentRange = (currentRange + 1) % 4;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Backtab) { // Shift+Tab
currentRange = (currentRange - 1 + 4) % 4;
event.accepted = true;
return;
}
// Navigation Up/Down
if (checkKey(event, 'up')) {
moveSelection(-1);
event.accepted = true;
return;
}
if (checkKey(event, 'down')) {
moveSelection(1);
event.accepted = true;
return;
}
// Action Navigation Left/Right
if (checkKey(event, 'left')) {
moveAction(-1);
event.accepted = true;
return;
}
if (checkKey(event, 'right')) {
moveAction(1);
event.accepted = true;
return;
}
// Activation (Enter)
if (checkKey(event, 'enter')) {
activateSelection();
event.accepted = true;
return;
}
// Removal (Delete/Remove)
if (checkKey(event, 'keyRemove') || event.key === Qt.Key_Delete) {
removeSelection();
event.accepted = true;
return;
}
}
function moveSelection(dir) {
var m = NotificationService.historyList;
if (!m || m.count === 0)
return;
var newIndex = focusIndex;
var found = false;
var count = m.count;
// If no selection yet, start from beginning (or end if up)
if (focusIndex === -1) {
if (dir > 0)
newIndex = -1;
else
newIndex = count;
}
// Loop to find next visible item
var loopCount = 0;
while (loopCount < count) {
newIndex += dir;
// Bounds check
if (newIndex < 0 || newIndex >= count) {
break; // Stop at edges
}
var item = m.get(newIndex);
if (item && isInCurrentRange(item.timestamp)) {
found = true;
break;
}
loopCount++;
}
if (found) {
focusIndex = newIndex;
actionIndex = -1; // Reset action selection
scrollToItem(focusIndex);
}
}
function moveAction(dir) {
if (focusIndex === -1)
return;
var item = NotificationService.historyList.get(focusIndex);
if (!item)
return;
// Parse actions
var actions = [];
try {
actions = JSON.parse(item.actionsJson || "[]");
} catch (e) {}
if (actions.length === 0)
return;
var newActionIndex = actionIndex + dir;
// Clamp between -1 (body) and actions.length - 1
if (newActionIndex < -1)
newActionIndex = -1;
if (newActionIndex >= actions.length)
newActionIndex = actions.length - 1;
actionIndex = newActionIndex;
}
function activateSelection() {
if (focusIndex === -1)
return;
var item = NotificationService.historyList.get(focusIndex);
if (!item)
return;
if (actionIndex >= 0) {
// Invoke action
var actions = [];
try {
actions = JSON.parse(item.actionsJson || "[]");
} catch (e) {}
if (actionIndex < actions.length) {
NotificationService.invokeAction(item.id, actions[actionIndex].identifier);
}
} else {
// Toggle expansion or open?
// User request didn't specify. Let's toggle expansion logic if we can access it.
// We can communicate with the delegate via a property or signal?
// Delegates read 'scrollView.expandedId'.
if (scrollView.expandedId === item.id) {
scrollView.expandedId = "";
} else {
scrollView.expandedId = item.id;
}
}
}
function removeSelection() {
if (focusIndex === -1)
return;
var item = NotificationService.historyList.get(focusIndex);
if (!item)
return;
NotificationService.removeFromHistory(item.id);
// selection updates automatically?
// If we remove item at index i, the next item becomes index i.
// So focusIndex is still valid (unless it was last item).
// But we should re-verify if it exists.
// Actually NotificationService removal might be async or immediate.
// If immediate, model count decreases.
// We might need to clamp focusIndex.
// Let's handle this in a helper or just let the user navigate again.
// Better UX: select next available or previous if last.
}
function scrollToItem(index) {
// Find the delegate item
if (index < 0 || index >= notificationColumn.children.length)
return;
var item = notificationColumn.children[index];
if (item && item.visible) {
// Use the internal flickable from NScrollView for accurate scrolling
var flickable = scrollView._internalFlickable;
if (!flickable || !flickable.contentItem)
return;
var pos = flickable.contentItem.mapFromItem(item, 0, 0);
var itemY = pos.y;
var itemHeight = item.height;
var currentContentY = flickable.contentY;
var viewHeight = flickable.height;
// Check if above visible area
if (itemY < currentContentY) {
flickable.contentY = Math.max(0, itemY - Style.marginM);
} else
// Check if below visible area
if (itemY + itemHeight > currentContentY + viewHeight) {
flickable.contentY = (itemY + itemHeight) - viewHeight + Style.marginM;
}
}
}
// Calculate content height based on header + tabs (if visible) + content
property real calculatedHeight: {
@@ -41,6 +251,20 @@ SmartPanel {
// 0 = All, 1 = Today, 2 = Yesterday, 3 = Earlier
property int currentRange: 1 // start on Today by default
property bool groupByDate: true
onCurrentRangeChanged: resetFocus()
// Keyboard navigation state
property int focusIndex: -1
property int actionIndex: -1 // For actions within a notification
function resetFocus() {
focusIndex = -1;
actionIndex = -1;
}
function checkKey(event, settingName) {
return Keybinds.checkKey(event, settingName, Settings);
}
// Helper functions (lazy-loaded with panelContent)
function dateOnly(d) {
@@ -277,7 +501,7 @@ SmartPanel {
id: emptyState
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginL
spacing: Style.marginM
Item {
Layout.fillHeight: true
@@ -333,6 +557,7 @@ SmartPanel {
visible: panelContent.isInCurrentRange(model.timestamp)
height: visible && !isRemoving ? contentColumn.height + Style.marginXL : 0
property int listIndex: index
property string notificationId: model.id
property bool isExpanded: scrollView.expandedId === notificationId
property bool canExpand: summaryText.truncated || bodyText.truncated
@@ -415,12 +640,20 @@ SmartPanel {
}
}
property bool isFocused: index === panelContent.focusIndex
Rectangle {
anchors.fill: parent
radius: Style.radiusM
color: Color.mSurfaceVariant
border.color: Settings.data.ui.boxBorderEnabled ? Qt.alpha(Color.mOutline, Style.opacityHeavy) : "transparent"
border.width: Style.borderS
border.color: {
if (notificationDelegate.isFocused)
return Color.mPrimary;
if (Settings.data.ui.boxBorderEnabled)
return Qt.alpha(Color.mOutline, Style.opacityHeavy);
return "transparent";
}
border.width: notificationDelegate.isFocused ? Style.borderM : Style.borderS
Behavior on color {
enabled: !Settings.data.general.animationDisabled
@@ -438,6 +671,9 @@ SmartPanel {
enabled: !notificationDelegate.isRemoving
cursorShape: notificationDelegate.canExpand ? Qt.PointingHandCursor : Qt.ArrowCursor
onPressed: mouse => {
panelContent.focusIndex = index;
panelContent.actionIndex = -1;
if (mouse.button !== Qt.LeftButton)
return;
const globalPoint = historyInteractionArea.mapToGlobal(mouse.x, mouse.y);
@@ -641,14 +877,26 @@ SmartPanel {
Repeater {
model: notificationDelegate.actionsList
delegate: NButton {
text: modelData.text
fontSize: Style.fontSizeS
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
readonly property bool actionNavActive: notificationDelegate.isFocused && panelContent.actionIndex !== -1
readonly property bool isSelected: actionNavActive && panelContent.actionIndex === index
backgroundColor: isSelected ? Color.mSecondary : Color.mPrimary
textColor: isSelected ? Color.mOnSecondary : Color.mOnPrimary
outlined: false
implicitHeight: 24
onHoveredChanged: {
if (hovered) {
panelContent.focusIndex = notificationDelegate.listIndex;
}
}
// Capture modelData in a property to avoid reference errors
property var actionData: modelData
onClicked: {
@@ -664,7 +912,7 @@ SmartPanel {
icon: "trash"
tooltipText: I18n.tr("tooltips.delete-notification")
baseSize: Style.baseWidgetSize * 0.7
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
onClicked: {
NotificationService.removeFromHistory(notificationId);
-76
View File
@@ -1,76 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
Rectangle {
id: root
// Public properties
property string icon: ""
property string tooltipText: ""
property bool checked: false
property int tabIndex: 0
// Internal state
property bool isHovered: false
signal clicked
// Sizing
Layout.fillWidth: true
Layout.fillHeight: true
// Styling
radius: Style.iRadiusXS
color: root.checked ? Color.mPrimary : (root.isHovered ? Color.mHover : Color.mSurface)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
NIcon {
id: tabIcon
anchors.centerIn: parent
icon: root.icon
pointSize: Style.fontSizeL
color: root.checked ? Color.mOnPrimary : root.isHovered ? Color.mOnHover : Color.mOnSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
root.isHovered = true;
if (root.tooltipText) {
TooltipService.show(parent, root.tooltipText);
}
}
onExited: {
root.isHovered = false;
if (root.tooltipText) {
TooltipService.hide();
}
}
onClicked: {
if (root.tooltipText) {
TooltipService.hide();
}
root.clicked();
}
}
}
+67 -23
View File
@@ -7,8 +7,11 @@ import qs.Widgets
Rectangle {
id: root
// Public properties
// Public properties
property string text: ""
property string icon: ""
property string tooltipText: ""
property bool checked: false
property int tabIndex: 0
property real pointSize: Style.fontSizeM
@@ -22,7 +25,7 @@ Rectangle {
// Sizing
Layout.fillHeight: true
implicitWidth: tabText.implicitWidth + Style.marginXL
implicitWidth: contentLayout.implicitWidth + Style.marginXL
topLeftRadius: isFirst ? Style.iRadiusM : Style.iRadiusXXXS
bottomLeftRadius: isFirst ? Style.iRadiusM : Style.iRadiusXXXS
@@ -41,27 +44,57 @@ Rectangle {
}
}
NText {
id: tabText
y: Style.pixelAlignCenter(parent.height, height)
anchors {
left: parent.left
right: parent.right
leftMargin: Style.marginS
rightMargin: Style.marginS
}
text: root.text
pointSize: root.pointSize
font.weight: Style.fontWeightSemiBold
color: root.isHovered ? Color.mOnHover : (root.checked ? Color.mOnPrimary : Color.mOnSurface)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// Content
RowLayout {
id: contentLayout
anchors.centerIn: parent
width: Math.min(implicitWidth, parent.width - (Style.marginS * 2))
spacing: (root.icon !== "" && root.text !== "") ? Style.marginXS : 0
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
NIcon {
visible: root.icon !== ""
Layout.alignment: Qt.AlignVCenter
icon: root.icon
pointSize: root.pointSize * 1.2
color: root.isHovered ? Color.mOnHover : (root.checked ? Color.mOnPrimary : Color.mOnSurface)
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
NText {
id: tabText
visible: root.text !== ""
Layout.alignment: Qt.AlignVCenter
text: root.text
pointSize: root.pointSize
font.weight: Style.fontWeightSemiBold
color: root.isHovered ? Color.mOnHover : (root.checked ? Color.mOnPrimary : Color.mOnSurface)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
}
// Tooltip
Timer {
id: tooltipTimer
interval: 500
onTriggered: {
if (root.isHovered && root.tooltipText !== "") {
TooltipService.show(root, root.tooltipText);
}
}
}
@@ -70,8 +103,19 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
onEntered: {
root.isHovered = true;
if (root.tooltipText !== "") {
tooltipTimer.start();
}
}
onExited: {
root.isHovered = false;
tooltipTimer.stop();
if (root.tooltipText !== "") {
TooltipService.hide();
}
}
onClicked: {
root.clicked();
// Update parent NTabBar's currentIndex
+1
View File
@@ -24,6 +24,7 @@ import qs.Modules.LockScreen
import qs.Modules.MainScreen
import qs.Modules.Notification
import qs.Modules.OSD
import qs.Modules.Panels.Launcher
import qs.Modules.Panels.Settings
import qs.Modules.Toast