detachable sidebar

This commit is contained in:
end-4
2025-06-01 17:47:32 +02:00
parent a6a3a144d2
commit aaf652b17c
4 changed files with 146 additions and 75 deletions
@@ -16,7 +16,6 @@ import Quickshell.Hyprland
Item {
id: root
property var panelWindow
property var inputField: messageInputField
property string commandPrefix: "/"
@@ -17,7 +17,6 @@ import Quickshell.Hyprland
Item {
id: root
property var panelWindow
property var inputField: tagInputField
readonly property var responses: Booru.responses
property string previewDownloadPath: Directories.booruPreviews
@@ -17,25 +17,50 @@ Scope { // Scope
id: root
property int sidebarPadding: 15
property var tabButtonList: [{"icon": "neurology", "name": qsTr("Intelligence")}, {"icon": "bookmark_heart", "name": qsTr("Anime")}]
property int selectedTab: 0
property bool pin: false
property Component contentComponent: SidebarLeftContent {}
property Item sidebarContent
Component.onCompleted: {
root.sidebarContent = contentComponent.createObject(null, {
"scopeRoot": root,
});
sidebarLoader.item.contentParent.children = [root.sidebarContent];
}
onPinChanged: {
console.log("Sidebar pin state changed:", root.pin);
if (root.pin) {
sidebarContent.parent = null; // Detach content from sidebar
sidebarLoader.active = false; // Unload sidebar
detachedSidebarLoader.active = true; // Load detached window
detachedSidebarLoader.item.contentParent.children = [sidebarContent];
} else {
sidebarContent.parent = null; // Detach content from window
detachedSidebarLoader.active = false; // Unload detached window
sidebarLoader.active = true; // Load sidebar
sidebarLoader.item.contentParent.children = [sidebarContent];
}
}
Loader {
id: sidebarLoader
active: GlobalStates.sidebarLeftOpen
active: true
PanelWindow { // Window
sourceComponent: PanelWindow { // Window
id: sidebarRoot
visible: GlobalStates.sidebarLeftOpen
property int selectedTab: 0
property bool extend: false
property bool pin: false
property real sidebarWidth: sidebarRoot.extend ? Appearance.sizes.sidebarWidthExtended : Appearance.sizes.sidebarWidth
property var contentParent: sidebarLeftBackground
function hide() {
GlobalStates.sidebarLeftOpen = false
}
exclusiveZone: sidebarRoot.pin ? sidebarWidth : 0
exclusiveZone: 0
implicitWidth: Appearance.sizes.sidebarWidthExtended + Appearance.sizes.elevationMargin
WlrLayershell.namespace: "quickshell:sidebarLeft"
// Hyprland 0.49: OnDemand is Exclusive, Exclusive just breaks click-outside-to-close
@@ -55,22 +80,22 @@ Scope { // Scope
HyprlandFocusGrab { // Click outside to close
id: grab
windows: [ sidebarRoot ]
active: sidebarRoot.visible && !sidebarRoot.pin
active: sidebarRoot.visible
onActiveChanged: { // Focus the selected tab
if (active) swipeView.currentItem.forceActiveFocus()
if (active) sidebarLeftBackground.children[0].focusActiveItem()
}
onCleared: () => {
if (!active) sidebarRoot.hide()
}
}
// Background
// Content
StyledRectangularShadow {
target: sidebarLeftBackground
radius: sidebarLeftBackground.radius
}
Rectangle {
id: sidebarLeftBackground
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: Appearance.sizes.hyprlandGapsOut
@@ -79,89 +104,48 @@ Scope { // Scope
height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
color: Appearance.colors.colLayer0
radius: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1
focus: sidebarRoot.visible
Behavior on width {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Keys.onPressed: (event) => {
// console.log("Key pressed: " + event.key)
if (event.key === Qt.Key_Escape) {
sidebarRoot.hide();
}
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_PageDown) {
sidebarRoot.selectedTab = Math.min(sidebarRoot.selectedTab + 1, root.tabButtonList.length - 1)
}
else if (event.key === Qt.Key_PageUp) {
sidebarRoot.selectedTab = Math.max(sidebarRoot.selectedTab - 1, 0)
}
else if (event.key === Qt.Key_Tab) {
sidebarRoot.selectedTab = (sidebarRoot.selectedTab + 1) % root.tabButtonList.length;
}
else if (event.key === Qt.Key_Backtab) {
sidebarRoot.selectedTab = (sidebarRoot.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length;
}
else if (event.key === Qt.Key_O) {
if (event.key === Qt.Key_O) {
sidebarRoot.extend = !sidebarRoot.extend;
}
else if (event.key === Qt.Key_P) {
sidebarRoot.pin = !sidebarRoot.pin;
root.pin = !root.pin;
}
event.accepted = true;
}
}
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: sidebarPadding
spacing: sidebarPadding
Loader {
id: detachedSidebarLoader
active: false
PrimaryTabBar { // Tab strip
id: tabBar
tabButtonList: root.tabButtonList
externalTrackedTab: sidebarRoot.selectedTab
function onCurrentIndexChanged(currentIndex) {
sidebarRoot.selectedTab = currentIndex
sourceComponent: FloatingWindow {
id: detachedSidebarRoot
visible: GlobalStates.sidebarLeftOpen
property var contentParent: detachedSidebarBackground
Rectangle {
id: detachedSidebarBackground
anchors.fill: parent
color: Appearance.colors.colLayer0
Keys.onPressed: (event) => {
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_P) {
root.pin = !root.pin;
}
event.accepted = true;
}
SwipeView { // Content pages
id: swipeView
Layout.topMargin: 5
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
currentIndex: tabBar.externalTrackedTab
onCurrentIndexChanged: {
tabBar.enableIndicatorAnimation = true
sidebarRoot.selectedTab = currentIndex
}
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView.width
height: swipeView.height
radius: Appearance.rounding.small
}
}
AiChat {
panelWindow: sidebarRoot
}
Anime {
panelWindow: sidebarRoot
}
}
}
}
}
}
@@ -0,0 +1,89 @@
import "root:/"
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
Item {
id: sidebarLeftBackground
required property var scopeRoot
anchors.fill: parent
function focusActiveItem() {
swipeView.currentItem.forceActiveFocus()
}
Keys.onPressed: (event) => {
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_PageDown) {
scopeRoot.selectedTab = Math.min(scopeRoot.selectedTab + 1, scopeRoot.tabButtonList.length - 1)
event.accepted = true;
}
else if (event.key === Qt.Key_PageUp) {
scopeRoot.selectedTab = Math.max(scopeRoot.selectedTab - 1, 0)
event.accepted = true;
}
else if (event.key === Qt.Key_Tab) {
scopeRoot.selectedTab = (scopeRoot.selectedTab + 1) % scopeRoot.tabButtonList.length;
event.accepted = true;
}
else if (event.key === Qt.Key_Backtab) {
scopeRoot.selectedTab = (scopeRoot.selectedTab - 1 + scopeRoot.tabButtonList.length) % scopeRoot.tabButtonList.length;
event.accepted = true;
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: sidebarPadding
spacing: sidebarPadding
PrimaryTabBar { // Tab strip
id: tabBar
tabButtonList: scopeRoot.tabButtonList
externalTrackedTab: scopeRoot.selectedTab
function onCurrentIndexChanged(currentIndex) {
scopeRoot.selectedTab = currentIndex
}
}
SwipeView { // Content pages
id: swipeView
Layout.topMargin: 5
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
currentIndex: tabBar.externalTrackedTab
onCurrentIndexChanged: {
tabBar.enableIndicatorAnimation = true
scopeRoot.selectedTab = currentIndex
}
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView.width
height: swipeView.height
radius: Appearance.rounding.small
}
}
AiChat {}
Anime {}
}
}
}