Files
noctalia-shell/Modules/Panels/Settings/SettingsContent.qml
T

1243 lines
40 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Panels.Settings.Tabs
import qs.Modules.Panels.Settings.Tabs.About
import qs.Modules.Panels.Settings.Tabs.Audio
import qs.Modules.Panels.Settings.Tabs.Bar
import qs.Modules.Panels.Settings.Tabs.ColorScheme
import qs.Modules.Panels.Settings.Tabs.ControlCenter
import qs.Modules.Panels.Settings.Tabs.Display
import qs.Modules.Panels.Settings.Tabs.Dock
import qs.Modules.Panels.Settings.Tabs.Hooks
import qs.Modules.Panels.Settings.Tabs.Launcher
import qs.Modules.Panels.Settings.Tabs.Notifications
import qs.Modules.Panels.Settings.Tabs.Osd
import qs.Modules.Panels.Settings.Tabs.Plugins
import qs.Modules.Panels.Settings.Tabs.Region
import qs.Modules.Panels.Settings.Tabs.SessionMenu
import qs.Modules.Panels.Settings.Tabs.SystemMonitor
import qs.Modules.Panels.Settings.Tabs.UserInterface
import qs.Modules.Panels.Settings.Tabs.Wallpaper
import qs.Services.System
import qs.Services.UI
import qs.Widgets
Item {
id: root
// Screen reference for child components
property var screen
// Input: which tab to show initially
property int requestedTab: 0
// Exposed state for parent to access
property int currentTabIndex: 0
property var tabsModel: []
property var activeScrollView: null
property var activeTabContent: null
property bool sidebarExpanded: true
// Track if sidebar was collapsed before searching started
property bool wasCollapsedBeforeSearch: false
// Search state
property string searchText: ""
property var searchIndex: []
property var searchResults: []
property int searchSelectedIndex: 0
property string highlightLabelKey: ""
// Mouse hover suppression during keyboard navigation
property bool ignoreMouseHover: false
property real _lastMouseX: 0
property real _lastMouseY: 0
property bool _mouseInitialized: false
onSearchResultsChanged: {
searchSelectedIndex = 0;
ignoreMouseHover = true;
_mouseInitialized = false;
}
// Signal when close button is clicked
signal closeRequested
// Load search index
FileView {
id: searchIndexFile
path: Quickshell.shellDir + "/Assets/settings-search-index.json"
watchChanges: false
printErrors: false
onLoaded: {
try {
root.searchIndex = JSON.parse(text());
} catch (e) {
root.searchIndex = [];
}
}
}
// Search function
onSearchTextChanged: {
if (searchText.trim() === "") {
searchResults = [];
if (wasCollapsedBeforeSearch) {
root.sidebarExpanded = false;
wasCollapsedBeforeSearch = false;
}
return;
}
// Auto-expand sidebar when searching
if (!root.sidebarExpanded) {
if (root.activeFocus) {
// If we are typing and the sidebar is collapsed and focused, we assume the user is typing to search
wasCollapsedBeforeSearch = true;
}
root.sidebarExpanded = true;
}
if (searchIndex.length === 0)
return;
// Build searchable items with resolved translations
let items = [];
for (let j = 0; j < searchIndex.length; j++) {
const entry = searchIndex[j];
items.push({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,
"widget": entry.widget,
"tab": entry.tab,
"tabLabel": entry.tabLabel,
"subTab": entry.subTab,
"subTabLabel": entry.subTabLabel || null,
"label": I18n.tr(entry.labelKey),
"description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "",
"subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : ""
});
}
const results = FuzzySort.go(searchText.trim(), items, {
"keys": ["label", "subTabName", "description"],
"limit": 20,
"scoreFn": function (r) {
// r[0]=label, r[1]=subTabName, r[2]=description
// Boost subTabName matches by 1.5x
const labelScore = r[0].score;
const subTabScore = r[1].score * 1.5;
const descScore = r[2].score;
return Math.max(labelScore, subTabScore, descScore);
}
});
let extracted = [];
for (let i = 0; i < results.length; i++) {
extracted.push(results[i].obj);
}
searchResults = extracted;
}
// Navigate to a search result
property int _pendingSubTab: -1
function navigateToResult(entry) {
if (entry.tab < 0 || entry.tab >= tabsModel.length)
return;
highlightLabelKey = entry.labelKey;
_pendingSubTab = (entry.subTab !== null && entry.subTab !== undefined) ? entry.subTab : -1;
// Check if we're already on this tab
const alreadyOnTab = (currentTabIndex === entry.tab);
currentTabIndex = entry.tab;
if (alreadyOnTab && activeTabContent) {
// Tab is already loaded, apply subtab + highlight directly
if (_pendingSubTab >= 0) {
setSubTabIndex(_pendingSubTab);
_pendingSubTab = -1;
}
highlightScrollTimer.targetKey = highlightLabelKey;
highlightScrollTimer.restart();
}
// Clear highlight after a delay
highlightClearTimer.restart();
}
function searchSelectNext() {
if (searchResults.length === 0)
return;
ignoreMouseHover = true;
_mouseInitialized = false;
searchSelectedIndex = Math.min(searchSelectedIndex + 1, searchResults.length - 1);
searchResultsList.positionViewAtIndex(searchSelectedIndex, ListView.Contain);
}
function searchSelectPrevious() {
if (searchResults.length === 0)
return;
ignoreMouseHover = true;
_mouseInitialized = false;
searchSelectedIndex = Math.max(searchSelectedIndex - 1, 0);
searchResultsList.positionViewAtIndex(searchSelectedIndex, ListView.Contain);
}
function searchActivate() {
if (searchSelectedIndex >= 0 && searchSelectedIndex < searchResults.length) {
navigateToResult(searchResults[searchSelectedIndex]);
searchInput.text = "";
}
}
// Set sub-tab on the currently loaded tab content
function setSubTabIndex(subTabIndex) {
if (activeTabContent) {
setSubTabRecursive(activeTabContent, subTabIndex);
}
}
function setSubTabRecursive(item, subTabIndex) {
if (!item)
return false;
if (item.objectName === "NTabBar") {
item.currentIndex = subTabIndex;
return true;
}
const childCount = item.children ? item.children.length : 0;
for (let i = 0; i < childCount; i++) {
if (setSubTabRecursive(item.children[i], subTabIndex))
return true;
}
return false;
}
// Find and highlight a widget by its label key
function findAndHighlightWidget(item, labelKey) {
if (!item)
return null;
// Check if this item has a matching label
if (item.hasOwnProperty("label") && item.label === I18n.tr(labelKey)) {
return item;
}
// Recursively search children
if (item.children) {
for (let i = 0; i < item.children.length; i++) {
const found = findAndHighlightWidget(item.children[i], labelKey);
if (found)
return found;
}
}
return null;
}
Timer {
id: highlightClearTimer
interval: 3000
onTriggered: root.highlightLabelKey = ""
}
Timer {
id: highlightScrollTimer
interval: 333
property string targetKey: ""
onTriggered: {
if (root.activeTabContent && targetKey) {
const widget = root.findAndHighlightWidget(root.activeTabContent, targetKey);
if (widget && root.activeScrollView) {
// Scroll widget into view
const mapped = widget.mapToItem(root.activeScrollView.contentItem, 0, 0);
const scrollBar = root.activeScrollView.ScrollBar.vertical;
if (scrollBar) {
const targetPos = (mapped.y - root.activeScrollView.height / 3) / root.activeScrollView.contentHeight;
scrollBar.position = Math.max(0, Math.min(targetPos, 1.0 - scrollBar.size));
}
// Position highlight overlay
const overlayPos = widget.mapToItem(tabContentArea, 0, 0);
highlightOverlay.x = overlayPos.x - Style.marginM;
highlightOverlay.y = overlayPos.y - Style.marginM;
highlightOverlay.width = widget.width + Style.marginM * 2;
highlightOverlay.height = widget.height + Style.marginM * 2;
highlightAnimation.restart();
}
}
targetKey = "";
}
}
// Save sidebar state when it changes
onSidebarExpandedChanged: {
ShellState.setSettingsSidebarExpanded(sidebarExpanded);
if (!sidebarExpanded) {
root.searchText = "";
searchInput.text = "";
root.forceActiveFocus();
}
}
Component.onCompleted: {
// Restore sidebar state
sidebarExpanded = ShellState.getSettingsSidebarExpanded();
}
// Tab components
Component {
id: generalTab
GeneralTab {}
}
Component {
id: launcherTab
LauncherTab {}
}
Component {
id: barTab
BarTab {}
}
Component {
id: audioTab
AudioTab {}
}
Component {
id: displayTab
DisplayTab {}
}
Component {
id: osdTab
OsdTab {}
}
Component {
id: networkTab
NetworkTab {}
}
Component {
id: regionTab
RegionTab {}
}
Component {
id: colorSchemeTab
ColorSchemeTab {}
}
Component {
id: wallpaperTab
WallpaperTab {}
}
Component {
id: aboutTab
AboutTab {}
}
Component {
id: hooksTab
HooksTab {}
}
Component {
id: dockTab
DockTab {}
}
Component {
id: notificationsTab
NotificationsTab {}
}
Component {
id: controlCenterTab
ControlCenterTab {}
}
Component {
id: userInterfaceTab
UserInterfaceTab {}
}
Component {
id: lockScreenTab
LockScreenTab {}
}
Component {
id: sessionMenuTab
SessionMenuTab {}
}
Component {
id: systemMonitorTab
SystemMonitorTab {}
}
Component {
id: pluginsTab
PluginsTab {}
}
Component {
id: desktopWidgetsTab
DesktopWidgetsTab {}
}
function updateTabsModel() {
let newTabs = [
{
"id": SettingsPanel.Tab.General,
"label": "common.general",
"icon": "settings-general",
"source": generalTab
},
{
"id": SettingsPanel.Tab.UserInterface,
"label": "panels.user-interface.title",
"icon": "settings-user-interface",
"source": userInterfaceTab
},
{
"id": SettingsPanel.Tab.ColorScheme,
"label": "panels.color-scheme.title",
"icon": "settings-color-scheme",
"source": colorSchemeTab
},
{
"id": SettingsPanel.Tab.Wallpaper,
"label": "common.wallpaper",
"icon": "settings-wallpaper",
"source": wallpaperTab
},
{
"id": SettingsPanel.Tab.Bar,
"label": "panels.bar.title",
"icon": "settings-bar",
"source": barTab
},
{
"id": SettingsPanel.Tab.Dock,
"label": "panels.dock.title",
"icon": "settings-dock",
"source": dockTab
},
{
"id": SettingsPanel.Tab.DesktopWidgets,
"label": "panels.desktop-widgets.title",
"icon": "clock",
"source": desktopWidgetsTab
},
{
"id": SettingsPanel.Tab.ControlCenter,
"label": "panels.control-center.title",
"icon": "settings-control-center",
"source": controlCenterTab
},
{
"id": SettingsPanel.Tab.Launcher,
"label": "panels.launcher.title",
"icon": "settings-launcher",
"source": launcherTab
},
{
"id": SettingsPanel.Tab.Notifications,
"label": "common.notifications",
"icon": "settings-notifications",
"source": notificationsTab
},
{
"id": SettingsPanel.Tab.OSD,
"label": "panels.osd.title",
"icon": "settings-osd",
"source": osdTab
},
{
"id": SettingsPanel.Tab.LockScreen,
"label": "panels.lock-screen.title",
"icon": "settings-lock-screen",
"source": lockScreenTab
},
{
"id": SettingsPanel.Tab.SessionMenu,
"label": "session-menu.title",
"icon": "settings-session-menu",
"source": sessionMenuTab
},
{
"id": SettingsPanel.Tab.Audio,
"label": "panels.audio.title",
"icon": "settings-audio",
"source": audioTab
},
{
"id": SettingsPanel.Tab.Display,
"label": "panels.display.title",
"icon": "settings-display",
"source": displayTab
},
{
"id": SettingsPanel.Tab.Network,
"label": "common.network",
"icon": "settings-network",
"source": networkTab
},
{
"id": SettingsPanel.Tab.Location,
"label": "panels.region.title",
"icon": "settings-location",
"source": regionTab
},
{
"id": SettingsPanel.Tab.SystemMonitor,
"label": "system-monitor.title",
"icon": "settings-system-monitor",
"source": systemMonitorTab
},
{
"id": SettingsPanel.Tab.Plugins,
"label": "panels.plugins.title",
"icon": "plugin",
"source": pluginsTab
},
{
"id": SettingsPanel.Tab.Hooks,
"label": "panels.hooks.title",
"icon": "settings-hooks",
"source": hooksTab
},
{
"id": SettingsPanel.Tab.About,
"label": "panels.about.title",
"icon": "settings-about",
"source": aboutTab
}
];
root.tabsModel = newTabs;
}
function selectTabById(tabId) {
for (var i = 0; i < tabsModel.length; i++) {
if (tabsModel[i].id === tabId) {
currentTabIndex = i;
return;
}
}
currentTabIndex = 0;
}
function initialize() {
ProgramCheckerService.checkAllPrograms();
updateTabsModel();
selectTabById(requestedTab);
if (sidebarExpanded) {
Qt.callLater(() => {
if (searchInput.inputItem)
searchInput.inputItem.forceActiveFocus();
});
} else {
// Ensure root has focus so it can catch typing
Qt.callLater(() => root.forceActiveFocus());
}
}
// Handle typing when sidebar is collapsed
focus: true
Keys.onPressed: event => {
if (!sidebarExpanded && event.text.length > 0 && event.text.trim() !== "") {
// Only capture if it looks like visible text
if (event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier))
return;
// Explicitly ignore backspace and similar keys that might have text but shouldn't trigger search
if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete || event.key === Qt.Key_Escape)
return;
wasCollapsedBeforeSearch = true;
sidebarExpanded = true;
searchInput.text = event.text;
Qt.callLater(() => {
if (searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus();
// Cursor moves to end automatically usually, but let's be safe
searchInput.inputItem.cursorPosition = 1;
}
});
event.accepted = true;
}
}
// Scroll functions
function scrollDown() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical;
const stepSize = activeScrollView.height * 0.1;
scrollBar.position = Math.min(scrollBar.position + stepSize / activeScrollView.contentHeight, 1.0 - scrollBar.size);
}
}
function scrollUp() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical;
const stepSize = activeScrollView.height * 0.1;
scrollBar.position = Math.max(scrollBar.position - stepSize / activeScrollView.contentHeight, 0);
}
}
function scrollPageDown() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical;
const pageSize = activeScrollView.height * 0.9;
scrollBar.position = Math.min(scrollBar.position + pageSize / activeScrollView.contentHeight, 1.0 - scrollBar.size);
}
}
function scrollPageUp() {
if (activeScrollView && activeScrollView.ScrollBar.vertical) {
const scrollBar = activeScrollView.ScrollBar.vertical;
const pageSize = activeScrollView.height * 0.9;
scrollBar.position = Math.max(scrollBar.position - pageSize / activeScrollView.contentHeight, 0);
}
}
// Tab navigation functions
function selectNextTab() {
if (tabsModel.length > 0) {
currentTabIndex = (currentTabIndex + 1) % tabsModel.length;
}
}
function selectPreviousTab() {
if (tabsModel.length > 0) {
currentTabIndex = (currentTabIndex - 1 + tabsModel.length) % tabsModel.length;
}
}
// Main UI
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: 0
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Style.marginL
// Sidebar
NBox {
id: sidebar
readonly property bool panelVeryTransparent: Settings.data.ui.panelBackgroundOpacity <= 0.75
clip: true
Layout.preferredWidth: Math.round(root.sidebarExpanded ? 200 * Style.uiScaleRatio : sidebarToggle.width + (panelVeryTransparent ? Style.marginXL : 0) + (sidebarList.verticalScrollBarActive ? Style.marginM : 0))
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
radius: sidebar.panelVeryTransparent ? Style.radiusM : 0
color: sidebar.panelVeryTransparent ? Color.mSurfaceVariant : "transparent"
border.color: sidebar.panelVeryTransparent ? Style.boxBorderColor : "transparent"
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
// Sidebar content
ColumnLayout {
anchors.fill: parent
spacing: Style.marginS
anchors.margins: sidebar.panelVeryTransparent ? Style.marginM : 0
// Sidebar toggle button
Item {
id: toggleContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(toggleRow.implicitHeight + Style.marginS * 2)
Rectangle {
id: sidebarToggle
width: Math.round(toggleRow.implicitWidth + Style.marginS * 2)
height: parent.height
anchors.left: parent.left
radius: Style.radiusS
color: toggleMouseArea.containsMouse ? Color.mHover : "transparent"
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
RowLayout {
id: toggleRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Style.marginS
spacing: 0
NIcon {
icon: root.sidebarExpanded ? "layout-sidebar-right-expand" : "layout-sidebar-left-expand"
color: toggleMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
pointSize: Style.fontSizeXL
}
}
MouseArea {
id: toggleMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
TooltipService.show(sidebarToggle, root.sidebarExpanded ? I18n.tr("tooltips.collapse") : I18n.tr("tooltips.expand"));
}
onExited: {
TooltipService.hide();
}
onClicked: {
TooltipService.hide();
root.sidebarExpanded = !root.sidebarExpanded;
}
}
}
}
// Search input
NTextInput {
id: searchInput
Layout.fillWidth: true
placeholderText: I18n.tr("common.search")
inputIconName: "search"
visible: root.sidebarExpanded
opacity: root.sidebarExpanded ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
onTextChanged: root.searchText = text
}
// Search button for collapsed sidebar
Item {
id: searchCollapsedContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(searchCollapsedRow.implicitHeight + Style.marginS * 2)
visible: !root.sidebarExpanded
opacity: !root.sidebarExpanded ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
Rectangle {
id: searchCollapsedButton
width: Math.round(searchCollapsedRow.implicitWidth + Style.marginS * 2)
height: parent.height
anchors.left: parent.left
radius: Style.radiusS
color: searchCollapsedMouseArea.containsMouse ? Color.mHover : "transparent"
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
RowLayout {
id: searchCollapsedRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Style.marginS
spacing: 0
NIcon {
icon: "search"
color: searchCollapsedMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
pointSize: Style.fontSizeXL
}
}
MouseArea {
id: searchCollapsedMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.sidebarExpanded = true;
root.wasCollapsedBeforeSearch = false; // Expanding manually resets this
Qt.callLater(() => searchInput.inputItem.forceActiveFocus());
}
onEntered: {
TooltipService.show(searchCollapsedButton, I18n.tr("common.search"));
}
onExited: {
TooltipService.hide();
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.bottomMargin: Style.marginXL
// Search results list
NListView {
id: searchResultsList
anchors.fill: parent
model: root.searchResults
spacing: Style.marginXS
visible: root.searchText.trim() !== ""
verticalPolicy: ScrollBar.AsNeeded
HoverHandler {
onPointChanged: {
if (!root._mouseInitialized) {
root._lastMouseX = point.position.x;
root._lastMouseY = point.position.y;
root._mouseInitialized = true;
return;
}
const deltaX = Math.abs(point.position.x - root._lastMouseX);
const deltaY = Math.abs(point.position.y - root._lastMouseY);
if (deltaX + deltaY >= 5) {
root.ignoreMouseHover = false;
root._lastMouseX = point.position.x;
root._lastMouseY = point.position.y;
}
}
}
delegate: Rectangle {
id: resultItem
width: searchResultsList.width - (searchResultsList.verticalScrollBarActive ? Style.marginM : 0)
height: resultColumn.implicitHeight + Style.marginS * 2
radius: Style.iRadiusS
readonly property bool selected: index === root.searchSelectedIndex
readonly property bool effectiveHover: !root.ignoreMouseHover && resultMouseArea.containsMouse
color: (effectiveHover || selected) ? Color.mHover : "transparent"
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
ColumnLayout {
id: resultColumn
anchors.fill: parent
anchors.leftMargin: Style.marginS
anchors.rightMargin: Style.marginS
anchors.topMargin: Style.marginXS
anchors.bottomMargin: Style.marginXS
spacing: 0
NText {
text: I18n.tr(modelData.labelKey)
pointSize: Style.fontSizeM
font.weight: Style.fontWeightSemiBold
color: (resultItem.effectiveHover || resultItem.selected) ? Color.mOnHover : Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
}
NText {
text: {
let t = I18n.tr(modelData.tabLabel);
if (modelData.subTabLabel)
t += " " + I18n.tr(modelData.subTabLabel);
return t;
}
pointSize: Style.fontSizeXS
color: (resultItem.effectiveHover || resultItem.selected) ? Color.mOnHover : Color.mOnSurfaceVariant
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: resultMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (!root.ignoreMouseHover)
root.searchSelectedIndex = index;
}
onClicked: {
root.searchSelectedIndex = index;
root.navigateToResult(modelData);
searchInput.text = "";
}
}
}
}
// Tab list
NListView {
id: sidebarList
visible: root.searchText.trim() === ""
anchors.fill: parent
model: root.tabsModel
spacing: Style.marginXS
currentIndex: root.currentTabIndex
verticalPolicy: ScrollBar.AsNeeded
delegate: Rectangle {
id: tabItem
width: sidebarList.width - (sidebarList.verticalScrollBarActive ? Style.marginM : 0)
height: tabEntryRow.implicitHeight + Style.marginS * 2
radius: Style.iRadiusS
color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mHover : "transparent")
readonly property bool selected: index === root.currentTabIndex
property bool hovering: false
property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnHover : Color.mOnSurface)
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
Behavior on tabTextColor {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
RowLayout {
id: tabEntryRow
anchors.fill: parent
anchors.leftMargin: Style.marginS
anchors.rightMargin: Style.marginS
spacing: Style.marginM
NIcon {
icon: modelData.icon
color: tabTextColor
pointSize: Style.fontSizeXL
Layout.alignment: Qt.AlignVCenter
}
NText {
text: I18n.tr(modelData.label)
color: tabTextColor
pointSize: Style.fontSizeM
font.weight: Style.fontWeightSemiBold
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
visible: root.sidebarExpanded
opacity: root.sidebarExpanded ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onEntered: {
tabItem.hovering = true;
// Show tooltip when sidebar is collapsed
if (!root.sidebarExpanded) {
TooltipService.show(tabItem, I18n.tr(modelData.label));
}
}
onExited: {
tabItem.hovering = false;
// Hide tooltip when sidebar is collapsed
if (!root.sidebarExpanded) {
TooltipService.hide();
}
}
onCanceled: {
tabItem.hovering = false;
if (!root.sidebarExpanded) {
TooltipService.hide();
}
}
onClicked: {
root.currentTabIndex = index;
// Hide tooltip on click
if (!root.sidebarExpanded) {
TooltipService.hide();
}
}
}
}
onCurrentIndexChanged: {
if (currentIndex !== root.currentTabIndex) {
root.currentTabIndex = currentIndex;
}
}
Connections {
target: root
function onCurrentTabIndexChanged() {
if (sidebarList.currentIndex !== root.currentTabIndex) {
sidebarList.currentIndex = root.currentTabIndex;
sidebarList.positionViewAtIndex(root.currentTabIndex, ListView.Contain);
}
}
}
}
}
}
// Overlay gradient for sidebar scrolling
Rectangle {
anchors.fill: parent
anchors.margins: Style.borderS
radius: Style.radiusM
color: "transparent"
visible: sidebarList.verticalScrollBarActive
opacity: (sidebarList.contentY + sidebarList.height >= sidebarList.contentHeight - 10) ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 0.95
color: "transparent"
}
GradientStop {
position: 1.0
color: Color.mSurfaceVariant
}
}
}
}
// Content pane
NBox {
id: contentPane
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignTop
radius: Style.radiusM
color: Color.mSurfaceVariant
ColumnLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginS
// Header row
RowLayout {
id: headerRow
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: root.tabsModel[currentTabIndex]?.icon ?? ""
color: Color.mPrimary
pointSize: Style.fontSizeXXL
}
NText {
text: root.tabsModel[root.currentTabIndex]?.label ? I18n.tr(root.tabsModel[root.currentTabIndex].label) : ""
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
NIconButton {
icon: "close"
tooltipText: I18n.tr("common.close")
Layout.alignment: Qt.AlignVCenter
onClicked: root.closeRequested()
}
}
// Tab content area
Rectangle {
id: tabContentArea
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: -Style.marginM
Layout.rightMargin: -Style.marginL
color: "transparent"
Repeater {
id: contentRepeater
model: root.tabsModel
delegate: Loader {
anchors.fill: parent
active: index === root.currentTabIndex
opacity: 0
NumberAnimation on opacity {
id: fadeInAnim
from: 0
to: 1
duration: Style.animationSlowest
easing.type: Easing.OutCubic
running: false
}
onStatusChanged: {
if (status === Loader.Ready && item) {
fadeInAnim.start();
const scrollView = item.children[0];
if (scrollView && scrollView.toString().includes("ScrollView")) {
root.activeScrollView = scrollView;
}
}
}
sourceComponent: Flickable {
id: flickable
anchors.fill: parent
pressDelay: 200
NScrollView {
id: scrollView
anchors.fill: parent
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
padding: Style.marginL
Component.onCompleted: {
root.activeScrollView = scrollView;
}
Loader {
active: true
sourceComponent: root.tabsModel[index]?.source
width: scrollView.availableWidth
onLoaded: {
if (item && item.hasOwnProperty("screen")) {
item.screen = root.screen;
}
root.activeTabContent = item;
// Handle pending subtab + highlight from search navigation
if (root.highlightLabelKey) {
if (root._pendingSubTab >= 0) {
root.setSubTabIndex(root._pendingSubTab);
root._pendingSubTab = -1;
}
highlightScrollTimer.targetKey = root.highlightLabelKey;
highlightScrollTimer.restart();
}
}
}
}
}
}
}
// Overlay gradient for content scrolling
Rectangle {
anchors.fill: parent
color: "transparent"
visible: root.activeScrollView && root.activeScrollView.ScrollBar.vertical && root.activeScrollView.ScrollBar.vertical.size < 1.0
opacity: {
if (!root.activeScrollView)
return 1;
const scrollBar = root.activeScrollView.ScrollBar.vertical;
return (scrollBar.position + scrollBar.size >= 0.99) ? 0 : 1;
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 0.95
color: "transparent"
}
GradientStop {
position: 1.0
color: Qt.alpha(Color.mSurfaceVariant, 0.95)
}
}
}
// Highlight overlay for search results
Rectangle {
id: highlightOverlay
visible: opacity > 0
opacity: 0
color: Qt.alpha(Color.mSecondary, 0.2)
border.color: Qt.alpha(Color.mSecondary, 0.6)
border.width: Style.borderM
radius: Style.radiusS
z: 100
SequentialAnimation {
id: highlightAnimation
NumberAnimation {
target: highlightOverlay
property: "opacity"
to: 1.0
duration: Style.animationSlow
easing.type: Easing.OutQuad
}
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: highlightOverlay
property: "opacity"
to: 0
duration: Style.animationSlowest
easing.type: Easing.InQuad
}
}
}
}
}
}
}
}
}