|
|
|
|
@ -1,16 +1,17 @@
|
|
|
|
|
import QtQuick 2.12
|
|
|
|
|
import QtQuick.Controls 2.12
|
|
|
|
|
import Qt5Compat.GraphicalEffects
|
|
|
|
|
import FishUI 1.0 as FishUI
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: control
|
|
|
|
|
property var tabs: []
|
|
|
|
|
property int currentIndex: 0
|
|
|
|
|
// Model index of the tab being dragged, and the slot it currently occupies.
|
|
|
|
|
property int dragIndex: -1
|
|
|
|
|
property int dropIndex: -1
|
|
|
|
|
property real pointerX: 0
|
|
|
|
|
property string dragTitle: ""
|
|
|
|
|
property real dragPos: 0
|
|
|
|
|
property real dragOffset: 0
|
|
|
|
|
signal selected(int index)
|
|
|
|
|
signal closeRequested(int index)
|
|
|
|
|
signal newRequested()
|
|
|
|
|
@ -18,15 +19,25 @@ Rectangle {
|
|
|
|
|
signal moveRequested(int from, int to)
|
|
|
|
|
|
|
|
|
|
color: FishUI.Theme.secondBackgroundColor
|
|
|
|
|
readonly property color separatorColor: Qt.rgba(FishUI.Theme.textColor.r,
|
|
|
|
|
readonly property color pressedColor: Qt.rgba(FishUI.Theme.textColor.r,
|
|
|
|
|
FishUI.Theme.textColor.g, FishUI.Theme.textColor.b, 0.10)
|
|
|
|
|
readonly property color hoverColor: Qt.rgba(FishUI.Theme.textColor.r,
|
|
|
|
|
FishUI.Theme.textColor.g, FishUI.Theme.textColor.b, 0.05)
|
|
|
|
|
|
|
|
|
|
function updateDrop(x) {
|
|
|
|
|
// Where a tab sits while a drag is in progress: the dragged one is taken
|
|
|
|
|
// out of the order and put back at dropIndex, so the rest slide aside.
|
|
|
|
|
function slotFor(index) {
|
|
|
|
|
if (dragIndex < 0 || index === dragIndex)
|
|
|
|
|
return index
|
|
|
|
|
var without = index - (index > dragIndex ? 1 : 0)
|
|
|
|
|
return without + (without >= dropIndex ? 1 : 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateDrag(x) {
|
|
|
|
|
pointerX = x
|
|
|
|
|
dropIndex = Math.max(0, Math.min(tabs.length - 1,
|
|
|
|
|
Math.floor((x - list.x + list.contentX) / list.tabWidth)))
|
|
|
|
|
var limit = Math.max(0, list.contentWidth - list.tabWidth)
|
|
|
|
|
dragPos = Math.max(0, Math.min(limit, x - list.x + list.contentX - dragOffset))
|
|
|
|
|
dropIndex = Math.max(0, Math.min(tabs.length - 1, Math.round(dragPos / list.tabWidth)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
@ -38,194 +49,184 @@ Rectangle {
|
|
|
|
|
color: FishUI.Theme.alternateBackgroundColor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ListView {
|
|
|
|
|
Flickable {
|
|
|
|
|
id: list
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.leftMargin: 6
|
|
|
|
|
anchors.rightMargin: 40
|
|
|
|
|
anchors.topMargin: 4
|
|
|
|
|
anchors.bottomMargin: 4
|
|
|
|
|
orientation: ListView.Horizontal
|
|
|
|
|
layoutDirection: Qt.LeftToRight
|
|
|
|
|
clip: true
|
|
|
|
|
model: control.tabs
|
|
|
|
|
currentIndex: control.currentIndex
|
|
|
|
|
readonly property real tabWidth: Math.max(140, width / Math.max(1, count))
|
|
|
|
|
interactive: control.dragIndex < 0
|
|
|
|
|
flickableDirection: Flickable.HorizontalFlick
|
|
|
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
|
onCurrentIndexChanged: {
|
|
|
|
|
if (control.dragIndex < 0)
|
|
|
|
|
positionViewAtIndex(currentIndex, ListView.Contain)
|
|
|
|
|
contentWidth: tabWidth * count
|
|
|
|
|
contentHeight: height
|
|
|
|
|
|
|
|
|
|
readonly property int count: control.tabs.length
|
|
|
|
|
readonly property real tabWidth: Math.max(140, width / Math.max(1, count))
|
|
|
|
|
|
|
|
|
|
function ensureVisible(index) {
|
|
|
|
|
if (index < 0 || index >= count)
|
|
|
|
|
return
|
|
|
|
|
var left = index * tabWidth
|
|
|
|
|
var limit = Math.max(0, contentWidth - width)
|
|
|
|
|
contentX = Math.max(0, Math.min(limit,
|
|
|
|
|
left < contentX ? left
|
|
|
|
|
: left + tabWidth > contentX + width ? left + tabWidth - width
|
|
|
|
|
: contentX))
|
|
|
|
|
}
|
|
|
|
|
onCountChanged: Qt.callLater(function() { list.positionViewAtIndex(list.currentIndex, ListView.Contain) })
|
|
|
|
|
|
|
|
|
|
delegate: Item {
|
|
|
|
|
id: tab
|
|
|
|
|
objectName: "folderTab" + index
|
|
|
|
|
width: list.tabWidth
|
|
|
|
|
height: list.height
|
|
|
|
|
property bool selected: index === control.currentIndex
|
|
|
|
|
opacity: control.dragIndex === index ? 0.25 : 1
|
|
|
|
|
Repeater {
|
|
|
|
|
model: control.tabs
|
|
|
|
|
|
|
|
|
|
delegate: Item {
|
|
|
|
|
id: tab
|
|
|
|
|
objectName: "folderTab" + index
|
|
|
|
|
width: list.tabWidth
|
|
|
|
|
height: list.height
|
|
|
|
|
x: control.dragIndex === index ? control.dragPos
|
|
|
|
|
: control.slotFor(index) * list.tabWidth
|
|
|
|
|
z: control.dragIndex === index ? 2 : tab.selected ? 1 : 0
|
|
|
|
|
property bool selected: index === control.currentIndex
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.margins: 2
|
|
|
|
|
radius: height / 2
|
|
|
|
|
color: tab.selected ? (FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.secondBackgroundColor, 2)
|
|
|
|
|
: FishUI.Theme.secondBackgroundColor)
|
|
|
|
|
: mouse.containsMouse ? control.hoverColor : "transparent"
|
|
|
|
|
layer.enabled: tab.selected
|
|
|
|
|
layer.effect: DropShadow {
|
|
|
|
|
transparentBorder: true
|
|
|
|
|
radius: 3
|
|
|
|
|
samples: 6
|
|
|
|
|
horizontalOffset: 0
|
|
|
|
|
verticalOffset: 1
|
|
|
|
|
color: Qt.rgba(0, 0, 0, FishUI.Theme.darkMode ? 0.18 : 0.12)
|
|
|
|
|
// Only the shuffling aside during a drag animates; opening or
|
|
|
|
|
// closing a tab must place the rest at once.
|
|
|
|
|
Behavior on x {
|
|
|
|
|
enabled: control.dragIndex >= 0 && control.dragIndex !== index
|
|
|
|
|
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
id: mouse
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
preventStealing: true
|
|
|
|
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
|
|
|
|
property real pressX: 0
|
|
|
|
|
property bool didDrag: false
|
|
|
|
|
onPressed: function(event) {
|
|
|
|
|
didDrag = false
|
|
|
|
|
pressX = mapToItem(control, event.x, event.y).x
|
|
|
|
|
if (event.button === Qt.LeftButton)
|
|
|
|
|
control.selected(index)
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.margins: 2
|
|
|
|
|
radius: height / 2
|
|
|
|
|
color: tab.selected ? (FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.secondBackgroundColor, 2)
|
|
|
|
|
: FishUI.Theme.secondBackgroundColor)
|
|
|
|
|
: mouse.containsMouse ? control.hoverColor : "transparent"
|
|
|
|
|
}
|
|
|
|
|
onPositionChanged: function(event) {
|
|
|
|
|
if (!(pressedButtons & Qt.LeftButton))
|
|
|
|
|
return
|
|
|
|
|
var x = mapToItem(control, event.x, event.y).x
|
|
|
|
|
if (!didDrag && Math.abs(x - pressX) > 8) {
|
|
|
|
|
didDrag = true
|
|
|
|
|
control.dragIndex = index
|
|
|
|
|
control.dragTitle = modelData.tabTitle
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
id: mouse
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
preventStealing: true
|
|
|
|
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
|
|
|
|
property real pressX: 0
|
|
|
|
|
property bool didDrag: false
|
|
|
|
|
onPressed: function(event) {
|
|
|
|
|
didDrag = false
|
|
|
|
|
pressX = mapToItem(control, event.x, event.y).x
|
|
|
|
|
if (event.button === Qt.LeftButton) {
|
|
|
|
|
control.dragOffset = event.x
|
|
|
|
|
control.selected(index)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (didDrag)
|
|
|
|
|
control.updateDrop(x)
|
|
|
|
|
}
|
|
|
|
|
onReleased: {
|
|
|
|
|
if (didDrag) {
|
|
|
|
|
onPositionChanged: function(event) {
|
|
|
|
|
if (!(pressedButtons & Qt.LeftButton))
|
|
|
|
|
return
|
|
|
|
|
var x = mapToItem(control, event.x, event.y).x
|
|
|
|
|
if (!didDrag && Math.abs(x - pressX) > 8) {
|
|
|
|
|
didDrag = true
|
|
|
|
|
control.dragIndex = index
|
|
|
|
|
control.dropIndex = index
|
|
|
|
|
}
|
|
|
|
|
if (didDrag)
|
|
|
|
|
control.updateDrag(x)
|
|
|
|
|
}
|
|
|
|
|
onReleased: {
|
|
|
|
|
if (!didDrag)
|
|
|
|
|
return
|
|
|
|
|
var from = control.dragIndex
|
|
|
|
|
var to = control.dropIndex
|
|
|
|
|
// Clear first: the tabs are already in their final
|
|
|
|
|
// places, so the reorder must not animate them again.
|
|
|
|
|
control.dragIndex = -1
|
|
|
|
|
control.dropIndex = -1
|
|
|
|
|
control.moveRequested(from, to)
|
|
|
|
|
}
|
|
|
|
|
onCanceled: {
|
|
|
|
|
control.dragIndex = -1
|
|
|
|
|
control.dropIndex = -1
|
|
|
|
|
}
|
|
|
|
|
onClicked: function(event) {
|
|
|
|
|
if (didDrag)
|
|
|
|
|
return
|
|
|
|
|
if (event.button === Qt.MiddleButton)
|
|
|
|
|
control.closeRequested(index)
|
|
|
|
|
else if (event.button === Qt.RightButton)
|
|
|
|
|
menu.popup()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
onCanceled: {
|
|
|
|
|
control.dragIndex = -1
|
|
|
|
|
control.dropIndex = -1
|
|
|
|
|
}
|
|
|
|
|
onClicked: function(event) {
|
|
|
|
|
if (didDrag)
|
|
|
|
|
return
|
|
|
|
|
if (event.button === Qt.MiddleButton)
|
|
|
|
|
control.closeRequested(index)
|
|
|
|
|
else if (event.button === Qt.RightButton)
|
|
|
|
|
menu.popup()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.leftMargin: 28
|
|
|
|
|
anchors.rightMargin: 28
|
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
|
text: modelData.tabTitle
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
font.weight: tab.selected ? Font.Medium : Font.Normal
|
|
|
|
|
elide: Text.ElideMiddle
|
|
|
|
|
color: FishUI.Theme.textColor
|
|
|
|
|
}
|
|
|
|
|
Label {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.leftMargin: 28
|
|
|
|
|
anchors.rightMargin: 28
|
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
|
text: modelData.tabTitle
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
font.weight: tab.selected ? Font.Medium : Font.Normal
|
|
|
|
|
elide: Text.ElideMiddle
|
|
|
|
|
color: FishUI.Theme.textColor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ToolButton {
|
|
|
|
|
id: closeButton
|
|
|
|
|
objectName: "tabCloseButton"
|
|
|
|
|
width: 20
|
|
|
|
|
height: 20
|
|
|
|
|
anchors.right: parent.right
|
|
|
|
|
anchors.rightMargin: 6
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
opacity: tab.selected || mouse.containsMouse || hovered ? 1 : 0
|
|
|
|
|
Accessible.name: qsTr("Close Tab")
|
|
|
|
|
onClicked: control.closeRequested(index)
|
|
|
|
|
contentItem: Item {
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
width: 10
|
|
|
|
|
height: 1.2
|
|
|
|
|
rotation: 45
|
|
|
|
|
color: FishUI.Theme.textColor
|
|
|
|
|
ToolButton {
|
|
|
|
|
id: closeButton
|
|
|
|
|
objectName: "tabCloseButton"
|
|
|
|
|
width: 20
|
|
|
|
|
height: 20
|
|
|
|
|
anchors.right: parent.right
|
|
|
|
|
anchors.rightMargin: 6
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
visible: control.dragIndex < 0
|
|
|
|
|
opacity: tab.selected || mouse.containsMouse || hovered ? 1 : 0
|
|
|
|
|
Accessible.name: qsTr("Close Tab")
|
|
|
|
|
onClicked: control.closeRequested(index)
|
|
|
|
|
contentItem: Item {
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
width: 10
|
|
|
|
|
height: 1.2
|
|
|
|
|
rotation: 45
|
|
|
|
|
color: FishUI.Theme.textColor
|
|
|
|
|
}
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
width: 10
|
|
|
|
|
height: 1.2
|
|
|
|
|
rotation: -45
|
|
|
|
|
color: FishUI.Theme.textColor
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
width: 10
|
|
|
|
|
height: 1.2
|
|
|
|
|
rotation: -45
|
|
|
|
|
color: FishUI.Theme.textColor
|
|
|
|
|
background: Rectangle {
|
|
|
|
|
radius: height / 2
|
|
|
|
|
color: closeButton.down ? control.pressedColor
|
|
|
|
|
: closeButton.hovered ? control.hoverColor : "transparent"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
background: Rectangle {
|
|
|
|
|
radius: height / 2
|
|
|
|
|
color: closeButton.down ? control.separatorColor
|
|
|
|
|
: closeButton.hovered ? control.hoverColor : "transparent"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FishUI.DesktopMenu {
|
|
|
|
|
id: menu
|
|
|
|
|
FishUI.MenuItem {
|
|
|
|
|
text: qsTr("Open In New Tab")
|
|
|
|
|
onTriggered: control.duplicateRequested(modelData.currentUrl)
|
|
|
|
|
}
|
|
|
|
|
FishUI.MenuItem {
|
|
|
|
|
text: qsTr("Close Tab")
|
|
|
|
|
onTriggered: control.closeRequested(index)
|
|
|
|
|
FishUI.DesktopMenu {
|
|
|
|
|
id: menu
|
|
|
|
|
FishUI.MenuItem {
|
|
|
|
|
text: qsTr("Open In New Tab")
|
|
|
|
|
onTriggered: control.duplicateRequested(modelData.currentUrl)
|
|
|
|
|
}
|
|
|
|
|
FishUI.MenuItem {
|
|
|
|
|
text: qsTr("Close Tab")
|
|
|
|
|
onTriggered: control.closeRequested(index)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
visible: control.dragIndex >= 0
|
|
|
|
|
x: Math.max(list.x, Math.min(list.x + list.width - width, control.pointerX - width / 2))
|
|
|
|
|
y: 4
|
|
|
|
|
width: Math.min(list.tabWidth - 2, list.width)
|
|
|
|
|
height: list.height - 2
|
|
|
|
|
radius: height / 2
|
|
|
|
|
color: FishUI.Theme.darkMode ? Qt.lighter(FishUI.Theme.secondBackgroundColor, 2)
|
|
|
|
|
: FishUI.Theme.secondBackgroundColor
|
|
|
|
|
opacity: 0.95
|
|
|
|
|
Label {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.leftMargin: 12
|
|
|
|
|
anchors.rightMargin: 12
|
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
|
text: control.dragTitle
|
|
|
|
|
elide: Text.ElideMiddle
|
|
|
|
|
color: FishUI.Theme.textColor
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
visible: control.dragIndex >= 0 && control.dropIndex !== control.dragIndex
|
|
|
|
|
x: Math.max(list.x, Math.min(list.x + list.width - width,
|
|
|
|
|
list.x + (control.dropIndex + (control.dropIndex > control.dragIndex ? 1 : 0)) * list.tabWidth - list.contentX))
|
|
|
|
|
y: 6
|
|
|
|
|
width: 2
|
|
|
|
|
height: control.height - 12
|
|
|
|
|
radius: 1
|
|
|
|
|
color: FishUI.Theme.highlightColor
|
|
|
|
|
onCurrentIndexChanged: {
|
|
|
|
|
if (control.dragIndex < 0)
|
|
|
|
|
list.ensureVisible(control.currentIndex)
|
|
|
|
|
}
|
|
|
|
|
onTabsChanged: Qt.callLater(function() { list.ensureVisible(control.currentIndex) })
|
|
|
|
|
|
|
|
|
|
Timer {
|
|
|
|
|
interval: 30
|
|
|
|
|
@ -234,8 +235,11 @@ Rectangle {
|
|
|
|
|
onTriggered: {
|
|
|
|
|
var delta = control.pointerX < list.x + 24 ? -8
|
|
|
|
|
: control.pointerX > list.x + list.width - 24 ? 8 : 0
|
|
|
|
|
list.contentX = Math.max(0, Math.min(Math.max(0, list.contentWidth - list.width), list.contentX + delta))
|
|
|
|
|
control.updateDrop(control.pointerX)
|
|
|
|
|
if (delta === 0)
|
|
|
|
|
return
|
|
|
|
|
list.contentX = Math.max(0, Math.min(Math.max(0, list.contentWidth - list.width),
|
|
|
|
|
list.contentX + delta))
|
|
|
|
|
control.updateDrag(control.pointerX)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|