pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import qs.Common import qs.Services import qs.Widgets Column { id: root property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null property bool contentLoaded: false property int draggedIndex: -1 property int dropTargetIndex: -1 property bool suppressShiftAnimation: false property int editingIndex: -1 readonly property real tabItemSize: tabRow.dynamicTabWidth + Theme.spacingXS signal tabSwitched(int tabIndex) signal tabClosed(int tabIndex) signal newTabRequested function commitRename(index, newTitle) { if (index >= 0) NotepadStorageService.renameTab(index, newTitle); editingIndex = -1; } function hasUnsavedChangesForTab(tab) { if (!tab) return false; if (tab.id === currentTab?.id) { return root.parent?.hasUnsavedChanges ? root.parent.hasUnsavedChanges() : false; } return false; } spacing: Theme.spacingXS Row { id: tabRow width: parent.width height: 36 spacing: Theme.spacingXS readonly property real dynamicTabWidth: { var count = Math.max(1, NotepadStorageService.tabs.length); var raw = (tabScroll.width - (count - 1) * Theme.spacingXS) / count; return Math.max(128, Math.min(300, raw)); } ScrollView { id: tabScroll width: parent.width - newTabButton.width - Theme.spacingXS height: parent.height clip: true ScrollBar.horizontal.visible: false ScrollBar.vertical.visible: false Row { spacing: Theme.spacingXS Repeater { model: NotepadStorageService.tabs delegate: Item { id: delegateItem required property int index required property var modelData readonly property bool isActive: NotepadStorageService.currentTabIndex === index readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse readonly property bool editing: root.editingIndex === index readonly property real tabWidth: tabRow.dynamicTabWidth property bool longPressing: false property bool dragging: false property point dragStartPos: Qt.point(0, 0) property int targetIndex: -1 property int originalIndex: -1 property real dragAxisOffset: 0 Timer { id: longPressTimer interval: 200 repeat: false onTriggered: { if (NotepadStorageService.tabs.length > 1) { delegateItem.longPressing = true; } } } readonly property real shiftOffset: { if (root.draggedIndex < 0) return 0; if (index === root.draggedIndex) return 0; var dragIdx = root.draggedIndex; var dropIdx = root.dropTargetIndex; var myIdx = index; var shiftAmount = root.tabItemSize; if (dropIdx < 0) return 0; if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx) return -shiftAmount; if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx) return shiftAmount; return 0; } width: tabWidth height: 32 z: dragging ? 100 : 0 transform: Translate { x: shiftOffset Behavior on x { enabled: !root.suppressShiftAnimation NumberAnimation { duration: 150 easing.type: Easing.OutCubic } } } Item { id: tabVisual anchors.fill: parent z: 1 layer.enabled: dragging layer.smooth: true transform: Translate { x: dragging ? dragAxisOffset : 0 } Rectangle { id: tabRect anchors.fill: parent radius: Theme.cornerRadius color: isActive ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryPressed, 0) border.width: isActive || dragging ? 0 : 1 border.color: dragging ? Theme.primary : Theme.outlineMedium clip: true Row { id: tabContent anchors.fill: parent anchors.leftMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM spacing: Theme.spacingXS StyledText { id: tabText visible: !delegateItem.editing width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0) text: { var prefix = ""; if (hasUnsavedChangesForTab(modelData)) { prefix = "● "; } return prefix + (modelData.title || "Untitled"); } font.pixelSize: Theme.fontSizeSmall color: isActive ? Theme.primary : Theme.surfaceText font.weight: isActive ? Font.Medium : Font.Normal elide: Text.ElideMiddle maximumLineCount: 1 wrapMode: Text.NoWrap anchors.verticalCenter: parent.verticalCenter } TextInput { id: renameField visible: delegateItem.editing enabled: delegateItem.editing width: parent.width anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.primary selectionColor: Theme.primary selectedTextColor: Theme.background selectByMouse: true clip: true onEditingFinished: root.commitRename(index, text) Keys.onEscapePressed: event => { text = modelData.title || "Untitled"; root.editingIndex = -1; event.accepted = true; } // A tab switch re-focuses the editor via Qt.callLater; the // timer fires afterwards so the field keeps focus + selection. Timer { id: renameFocusTimer interval: 20 repeat: false onTriggered: { renameField.forceActiveFocus(); renameField.selectAll(); } } onVisibleChanged: { if (!visible) return; text = modelData.title || "Untitled"; renameFocusTimer.restart(); } } Rectangle { id: tabCloseButton width: 20 height: 20 radius: Theme.cornerRadius color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0) visible: NotepadStorageService.tabs.length > 1 && !delegateItem.editing anchors.verticalCenter: parent.verticalCenter DankIcon { name: "close" size: 14 color: Theme.surfaceTextMedium anchors.centerIn: parent } MouseArea { id: closeMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor z: 100 onClicked: root.tabClosed(index) } } } Behavior on color { ColorAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } } } MouseArea { id: tabMouseArea anchors.fill: parent enabled: !delegateItem.editing hoverEnabled: true preventStealing: dragging || longPressing cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor acceptedButtons: Qt.LeftButton onDoubleClicked: { root.tabSwitched(index); root.editingIndex = index; } onExited: tabTooltip.hide() onContainsMouseChanged: { if (containsMouse && tabText.truncated) tabTooltip.show(modelData.title || "Untitled", delegateItem, 0, 0, "bottom"); } onPressed: mouse => { if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) { delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y); longPressTimer.start(); } } onReleased: mouse => { longPressTimer.stop(); var wasDragging = delegateItem.dragging; var didReorder = wasDragging && delegateItem.targetIndex >= 0 && delegateItem.targetIndex !== delegateItem.originalIndex; if (didReorder) { root.suppressShiftAnimation = true; NotepadStorageService.reorderTab(delegateItem.originalIndex, delegateItem.targetIndex); } delegateItem.longPressing = false; delegateItem.dragging = false; delegateItem.dragAxisOffset = 0; delegateItem.targetIndex = -1; delegateItem.originalIndex = -1; root.draggedIndex = -1; root.dropTargetIndex = -1; if (didReorder) { Qt.callLater(() => { root.suppressShiftAnimation = false; }); } if (wasDragging || mouse.button !== Qt.LeftButton) return; root.tabSwitched(index); } onPositionChanged: mouse => { if (delegateItem.longPressing && !delegateItem.dragging) { var distance = Math.sqrt(Math.pow(mouse.x - delegateItem.dragStartPos.x, 2) + Math.pow(mouse.y - delegateItem.dragStartPos.y, 2)); if (distance > 5) { delegateItem.dragging = true; delegateItem.targetIndex = index; delegateItem.originalIndex = index; root.draggedIndex = index; root.dropTargetIndex = index; } } if (!delegateItem.dragging) return; var axisOffset = mouse.x - delegateItem.dragStartPos.x; delegateItem.dragAxisOffset = axisOffset; var itemSize = root.tabItemSize; var rawSlot = axisOffset / itemSize; var slotOffset = rawSlot >= 0 ? Math.floor(rawSlot + 0.4) : Math.ceil(rawSlot - 0.4); var tabCount = NotepadStorageService.tabs.length; var newTargetIndex = Math.max(0, Math.min(tabCount - 1, delegateItem.originalIndex + slotOffset)); if (newTargetIndex !== delegateItem.targetIndex) { delegateItem.targetIndex = newTargetIndex; root.dropTargetIndex = newTargetIndex; } } } } } } } DankActionButton { id: newTabButton width: 32 height: 32 iconName: "add" iconSize: Theme.iconSize - 4 iconColor: Theme.surfaceText onClicked: root.newTabRequested() } } DankTooltipV2 { id: tabTooltip } }