Qt Quick Controls - 文件系统浏览器#

本示例演示了如何创建一个具有统一外观的、现代观感且使用深色主题的用户界面文件系统浏览器。已实现自定义Qt Quick Controls,以提供用于从文件系统中打开和导航文本文件的干净直观的用户界面。

无框窗口#

为了最大化可用空间,我们使用了一个无框窗口。基本功能,如最小化、最大化窗口和关闭窗口,已经移动到名为MyMenuBar的定制菜单栏。由于在侧边栏和菜单栏中添加了WindowDragHandler,用户可以拖动窗口。

定制#

将自定义动画和颜色与QtQuick Controls相结合,我们可以轻松创建定制用户界面。本示例展示了QtQuick Controls在创建美观UI方面的潜力。

通过本例获得的知识,开发者可以应用类似的技术,使用 PySide 的 QtQuick 控件创建 他们自己的 定制 UI。

QtQuickControls Filesystem Explorer Screenshot

参考资料#

如果您对这个示例的 C++ 版本感兴趣,可以在以下位置找到:这里

此外,还有一个详细的 教程 可供参考,该教程提供了逐步指导,说明如何通过添加更多功能来扩展此示例。如果您想进一步探索和了解如何构建基于文件系统浏览程序现有功能的方法,本教程会很有帮助。

下载 示例

# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

"""
This example shows how to customize Qt Quick Controls by implementing a simple filesystem explorer.
"""

# Compile both resource files app.qrc and icons.qrc and include them here if you wish
# to load them from the resource system. Currently, all resources are loaded locally
# import FileSystemModule.rc_icons
# import FileSystemModule.rc_app

from editormodels import FileSystemModel  # noqa: F401
from PySide6.QtGui import QGuiApplication, QIcon
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QCommandLineParser, qVersion

import sys

if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    app.setOrganizationName("QtProject")
    app.setApplicationName("File System Explorer")
    app.setApplicationVersion(qVersion())
    app.setWindowIcon(QIcon("FileSystemModule/icons/app_icon.svg"))

    parser = QCommandLineParser()
    parser.setApplicationDescription("Qt Filesystemexplorer Example")
    parser.addHelpOption()
    parser.addVersionOption()
    parser.addPositionalArgument("", "Initial directory", "[path]")
    parser.process(app)
    args = parser.positionalArguments()

    engine = QQmlApplicationEngine()
    # Include the path of this file to search for the 'qmldir' module
    engine.addImportPath(sys.path[0])

    engine.loadFromModule("FileSystemModule", "Main")

    if not engine.rootObjects():
        sys.exit(-1)

    if (len(args) == 1):
        fsm = engine.singletonInstance("FileSystemModule", "FileSystemModel")
        fsm.setInitialDirectory(args[0])

    sys.exit(app.exec())
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtWidgets import QFileSystemModel
from PySide6.QtQuick import QQuickTextDocument
from PySide6.QtQml import QmlElement, QmlSingleton
from PySide6.QtCore import (Qt, QDir, QAbstractListModel, Slot, QFile, QTextStream,
                            QMimeDatabase, QFileInfo, QStandardPaths, QModelIndex,
                            Signal, Property)

QML_IMPORT_NAME = "FileSystemModule"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
@QmlSingleton
class FileSystemModel(QFileSystemModel):

    rootIndexChanged = Signal()

    def getDefaultRootDir():
        return QStandardPaths.writableLocation(QStandardPaths.StandardLocation.HomeLocation)

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.mRootIndex = QModelIndex()
        self.mDb = QMimeDatabase()
        self.setFilter(QDir.Filter.AllEntries | QDir.Filter.Hidden | QDir.Filter.NoDotAndDotDot)
        self.setInitialDirectory()

    # check for the correct mime type and then read the file.
    # returns the text file's content or an error message on failure
    @Slot(str, result=str)
    def readFile(self, path):
        if path == "":
            return ""

        file = QFile(path)

        mime = self.mDb.mimeTypeForFile(QFileInfo(file))
        if ('text' in mime.comment().lower()
                or any('text' in s.lower() for s in mime.parentMimeTypes())):
            if file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text):
                stream = QTextStream(file).readAll()
                file.close()
                return stream
            else:
                return self.tr("Error opening the file!")
        return self.tr("File type not supported!")

    @Slot(QQuickTextDocument, int, result=int)
    def currentLineNumber(self, textDocument, cursorPosition):
        td = textDocument.textDocument()
        tb = td.findBlock(cursorPosition)
        return tb.blockNumber()

    def setInitialDirectory(self, path=getDefaultRootDir()):
        dir = QDir(path)
        if dir.makeAbsolute():
            self.setRootPath(dir.path())
        else:
            self.setRootPath(self.getDefaultRootDir())
        self.setRootIndex(self.index(dir.path()))

    # we only need one column in this example
    def columnCount(self, parent):
        return 1

    @Property(QModelIndex, notify=rootIndexChanged)
    def rootIndex(self):
        return self.mRootIndex

    def setRootIndex(self, index):
        if (index == self.mRootIndex):
            return
        self.mRootIndex = index
        self.rootIndexChanged.emit()


@QmlElement
class LineNumberModel(QAbstractListModel):

    lineCountChanged = Signal()

    def __init__(self, parent=None):
        self.mLineCount = 0
        super().__init__(parent=parent)

    @Property(int, notify=lineCountChanged)
    def lineCount(self):
        return self.mLineCount

    @lineCount.setter
    def lineCount(self, n):
        if n < 0:
            print("lineCount must be greater then zero")
            return
        if self.mLineCount == n:
            return

        if self.mLineCount < n:
            self.beginInsertRows(QModelIndex(), self.mLineCount, n - 1)
            self.mLineCount = n
            self.endInsertRows()
        else:
            self.beginRemoveRows(QModelIndex(), n, self.mLineCount - 1)
            self.mLineCount = n
            self.endRemoveRows()

    def rowCount(self, parent):
        return self.mLineCount

    def data(self, index, role):
        if not self.checkIndex(index) or role != Qt.ItemDataRole.DisplayRole:
            return
        return index.row()
module FileSystemModule
Main 1.0 Main.qml
About 1.0 qml/About.qml
Editor 1.0 qml/Editor.qml
MyMenu 1.0 qml/MyMenu.qml
Sidebar 1.0 qml/Sidebar.qml
MyMenuBar 1.0 qml/MyMenuBar.qml
singleton Colors 1.0 qml/Colors.qml
ResizeButton 1.0 qml/ResizeButton.qml
FileSystemView 1.0 qml/FileSystemView.qml
WindowDragHandler 1.0 qml/WindowDragHandler.qml
<RCC>
    <qresource prefix="/qt/qml/FileSystemModule">
        <file>qmldir</file>
        <file>Main.qml</file>
        <file>qml/About.qml</file>
        <file>qml/Editor.qml</file>
        <file>qml/Colors.qml</file>
        <file>qml/FileSystemView.qml</file>
        <file>qml/MyMenu.qml</file>
        <file>qml/MyMenuBar.qml</file>
        <file>qml/ResizeButton.qml</file>
        <file>qml/Sidebar.qml</file>
        <file>qml/WindowDragHandler.qml</file>
    </qresource>
</RCC>
module FileSystemModule
Main 1.0 Main.qml
About 1.0 qml/About.qml
Editor 1.0 qml/Editor.qml
MyMenu 1.0 qml/MyMenu.qml
Sidebar 1.0 qml/Sidebar.qml
MyMenuBar 1.0 qml/MyMenuBar.qml
singleton Colors 1.0 qml/Colors.qml
ResizeButton 1.0 qml/ResizeButton.qml
FileSystemView 1.0 qml/FileSystemView.qml
WindowDragHandler 1.0 qml/WindowDragHandler.qml
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts
import FileSystemModule

pragma ComponentBehavior: Bound

ApplicationWindow {
    id: root

    property bool expandPath: false
    property bool showLineNumbers: true
    property string currentFilePath: ""

    width: 1100
    height: 600
    minimumWidth: 200
    minimumHeight: 100
    visible: true
    color: Colors.background
    flags: Qt.Window | Qt.FramelessWindowHint
    title: qsTr("File System Explorer Example")

    function getInfoText() : string {
        let out = root.currentFilePath
        if (!out)
            return qsTr("File System Explorer")
        return root.expandPath ? out : out.substring(out.lastIndexOf("/") + 1, out.length)
    }

    menuBar: MyMenuBar {
        dragWindow: root
        infoText: root.getInfoText()
        MyMenu {
            title: qsTr("File")

            Action {
                text: qsTr("Increase Font")
                shortcut: StandardKey.ZoomIn
                onTriggered: editor.text.font.pixelSize += 1
            }
            Action {
                text: qsTr("Decrease Font")
                shortcut: StandardKey.ZoomOut
                onTriggered: editor.text.font.pixelSize -= 1
            }
            Action {
                text: root.showLineNumbers ? qsTr("Toggle Line Numbers OFF")
                                           : qsTr("Toggle Line Numbers ON")
                shortcut: "Ctrl+L"
                onTriggered: root.showLineNumbers = !root.showLineNumbers
            }
            Action {
                text: root.expandPath ? qsTr("Toggle Short Path")
                                      : qsTr("Toggle Expand Path")
                enabled: root.currentFilePath
                onTriggered: root.expandPath = !root.expandPath
            }
            Action {
                text: qsTr("Reset Filesystem")
                enabled: sidebar.currentTabIndex === 1
                onTriggered: fileSystemView.rootIndex = undefined
            }
            Action {
                text: qsTr("Exit")
                onTriggered: Qt.exit(0)
                shortcut: StandardKey.Quit
            }
        }

        MyMenu {
            title: qsTr("Edit")

            Action {
                text: qsTr("Cut")
                shortcut: StandardKey.Cut
                enabled: editor.text.selectedText.length > 0
                onTriggered: editor.text.cut()
            }
            Action {
                text: qsTr("Copy")
                shortcut: StandardKey.Copy
                enabled: editor.text.selectedText.length > 0
                onTriggered: editor.text.copy()
            }
            Action {
                text: qsTr("Paste")
                shortcut: StandardKey.Paste
                enabled: editor.text.canPaste
                onTriggered: editor.text.paste()
            }
            Action {
                text: qsTr("Select All")
                shortcut: StandardKey.SelectAll
                enabled: editor.text.length > 0
                onTriggered: editor.text.selectAll()
            }
            Action {
                text: qsTr("Undo")
                shortcut: StandardKey.Undo
                enabled: editor.text.canUndo
                onTriggered: editor.text.undo()
            }
        }
    }
    // Set up the layout of the main components in a row:
    // [ Sidebar, Navigation, Editor ]
    RowLayout {
        anchors.fill: parent
        spacing: 0

        // Stores the buttons that navigate the application.
        Sidebar {
            id: sidebar
            dragWindow: root
            Layout.preferredWidth: 50
            Layout.fillHeight: true
        }

        // Allows resizing parts of the UI.
        SplitView {
            Layout.fillWidth: true
            Layout.fillHeight: true
            // Customized handle to drag between the Navigation and the Editor.
            handle: Rectangle {
                implicitWidth: 10
                color: SplitHandle.pressed ? Colors.color2 : Colors.background
                border.color: SplitHandle.hovered ? Colors.color2 : Colors.background
                opacity: SplitHandle.hovered || navigationView.width < 15 ? 1.0 : 0.0

                Behavior on opacity {
                    OpacityAnimator {
                        duration: 1400
                    }
                }
            }

            Rectangle {
                id: navigationView
                color: Colors.surface1
                SplitView.preferredWidth: 250
                SplitView.fillHeight: true
                // The stack-layout provides different views, based on the
                // selected buttons inside the sidebar.
                StackLayout {
                    anchors.fill: parent
                    currentIndex: sidebar.currentTabIndex

                    // Shows the help text.
                    Text {
                        text: qsTr("This example shows how to use and visualize the file system.\n\n"
                                 + "Customized Qt Quick Components have been used to achieve this look.\n\n"
                                 + "You can edit the files but they won't be changed on the file system.\n\n"
                                 + "Click on the folder icon to the left to get started.")
                        wrapMode: TextArea.Wrap
                        color: Colors.text
                    }

                    // Shows the files on the file system.
                    FileSystemView {
                        id: fileSystemView
                        color: Colors.surface1
                        onFileClicked: path => root.currentFilePath = path
                    }
                }
            }

            // The main view that contains the editor.
            Editor {
                id: editor
                showLineNumbers: root.showLineNumbers
                currentFilePath: root.currentFilePath
                SplitView.fillWidth: true
                SplitView.fillHeight: true
            }
        }
    }

    ResizeButton {
        resizeWindow: root
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls.Basic
import FileSystemModule

ApplicationWindow {
    id: root
    width: 650
    height: 550
    flags: Qt.Window | Qt.FramelessWindowHint
    color: Colors.surface1

    menuBar: MyMenuBar {
        id: menuBar

        dragWindow: root
        implicitHeight: 27
        infoText: "About Qt"
    }

    Image {
        id: logo

        anchors.left: parent.left
        anchors.right: parent.right
        anchors.top: parent.top
        anchors.margins: 20

        source: "../icons/qt_logo.svg"
        sourceSize.width: 80
        sourceSize.height: 80
        fillMode: Image.PreserveAspectFit

        smooth: true
        antialiasing: true
        asynchronous: true
    }

    ScrollView {
      anchors.top: logo.bottom
      anchors.left: parent.left
      anchors.right: parent.right
      anchors.bottom: parent.bottom
      anchors.margins: 20

      TextArea {
          selectedTextColor: Colors.textFile
          selectionColor: Colors.selection
          horizontalAlignment: Text.AlignHCenter
          textFormat: Text.RichText

          text: qsTr("<h3>About Qt</h3>"
                   + "<p>This program uses Qt version %1.</p>"
                   + "<p>Qt is a C++ toolkit for cross-platform application "
                   + "development.</p>"
                   + "<p>Qt provides single-source portability across all major desktop "
                   + "operating systems. It is also available for embedded Linux and other "
                   + "embedded and mobile operating systems.</p>"
                   + "<p>Qt is available under multiple licensing options designed "
                   + "to accommodate the needs of our various users.</p>"
                   + "<p>Qt licensed under our commercial license agreement is appropriate "
                   + "for development of proprietary/commercial software where you do not "
                   + "want to share any source code with third parties or otherwise cannot "
                   + "comply with the terms of GNU (L)GPL.</p>"
                   + "<p>Qt licensed under GNU (L)GPL is appropriate for the "
                   + "development of Qt&nbsp;applications provided you can comply with the terms "
                   + "and conditions of the respective licenses.</p>"
                   + "<p>Please see <a href=\"http://%2/\">%2</a> "
                   + "for an overview of Qt licensing.</p>"
                   + "<p>Copyright (C) %3 The Qt Company Ltd and other "
                   + "contributors.</p>"
                   + "<p>Qt and the Qt logo are trademarks of The Qt Company Ltd.</p>"
                   + "<p>Qt is The Qt Company Ltd product developed as an open source "
                   + "project. See <a href=\"http://%4/\">%4</a> for more information.</p>")
                   .arg(Application.version).arg("qt.io/licensing").arg("2023").arg("qt.io")
          color: Colors.textFile
          wrapMode: Text.WordWrap
          readOnly: true
          antialiasing: true
          background: null

          onLinkActivated: function(link) {
              Qt.openUrlExternally(link)
          }
      }
    }

    ResizeButton {
        resizeWindow: root
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick

pragma Singleton

QtObject {
    readonly property color background: "#292828"
    readonly property color surface1: "#171819"
    readonly property color surface2: "#090A0C"
    readonly property color text: "#D4BE98"
    readonly property color textFile: "#E1D2B7"
    readonly property color disabledText: "#2C313A"
    readonly property color selection: "#4B4A4A"
    readonly property color active: "#292828"
    readonly property color inactive: "#383737"
    readonly property color folder: "#383737"
    readonly property color icon: "#383737"
    readonly property color iconIndicator: "#D5B35D"
    readonly property color color1: "#A7B464"
    readonly property color color2: "#D3869B"
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import FileSystemModule

pragma ComponentBehavior: Bound

// This is the text editor that displays the currently open file, including
// their corresponding line numbers.
Rectangle {
    id: root

    required property string currentFilePath
    required property bool showLineNumbers
    property alias text: textArea
    property int currentLineNumber: -1
    property int rowHeight: Math.ceil(fontMetrics.lineSpacing)

    color: Colors.background

    onWidthChanged: textArea.update()
    onHeightChanged: textArea.update()

    RowLayout {
        anchors.fill: parent
        // We use a flickable to synchronize the position of the editor and
        // the line numbers. This is necessary because the line numbers can
        // extend the available height.
        Flickable {
            id: lineNumbers

            // Calculate the width based on the logarithmic scale.
            Layout.preferredWidth: fontMetrics.averageCharacterWidth
                * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
            Layout.fillHeight: true

            interactive: false
            contentY: editorFlickable.contentY
            visible: textArea.text !== "" && root.showLineNumbers

            Column {
                anchors.fill: parent
                Repeater {
                    id: repeatedLineNumbers

                    model: LineNumberModel {
                        lineCount: textArea.text !== "" ? textArea.lineCount : 0
                    }

                    delegate: Item {
                        required property int index

                        width: parent.width
                        height: root.rowHeight
                        Label {
                            id: numbers

                            text: parent.index + 1

                            width: parent.width
                            height: parent.height
                            horizontalAlignment: Text.AlignLeft
                            verticalAlignment: Text.AlignVCenter

                            color: (root.currentLineNumber === parent.index)
                                    ? Colors.iconIndicator : Qt.darker(Colors.text, 2)
                            font: textArea.font
                        }
                        Rectangle {
                            id: indicator

                            anchors.left: numbers.right
                            width: 1
                            height: parent.height
                            color: Qt.darker(Colors.text, 3)
                        }
                    }
                }
            }
        }

        Flickable {
            id: editorFlickable

            property alias textArea: textArea

            // We use an inline component to customize the horizontal and vertical
            // scroll-bars. This is convenient when the component is only used in one file.
            component MyScrollBar: ScrollBar {
                id: scrollBar
                background: Rectangle {
                    implicitWidth: scrollBar.interactive ? 8 : 4
                    implicitHeight: scrollBar.interactive ? 8 : 4

                    opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
                    color: Colors.background
                    Behavior on opacity {
                        OpacityAnimator {
                            duration: 500
                        }
                    }
                }
                contentItem: Rectangle {
                    implicitWidth: scrollBar.interactive ? 8 : 4
                    implicitHeight: scrollBar.interactive ? 8 : 4
                    opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
                    color: Colors.color1
                    Behavior on opacity {
                        OpacityAnimator {
                            duration: 1000
                        }
                    }
                }
            }

            Layout.fillHeight: true
            Layout.fillWidth: true
            ScrollBar.horizontal: MyScrollBar {}
            ScrollBar.vertical: MyScrollBar {}

            boundsBehavior: Flickable.StopAtBounds

            TextArea.flickable: TextArea {
                id: textArea
                anchors.fill: parent

                focus: false
                topPadding: 0
                leftPadding: 10

                text: FileSystemModel.readFile(root.currentFilePath)
                tabStopDistance: fontMetrics.averageCharacterWidth * 4

                // Grab the current line number from the C++ interface.
                onCursorPositionChanged: {
                    root.currentLineNumber = FileSystemModel.currentLineNumber(
                        textArea.textDocument, textArea.cursorPosition)
                }

                color: Colors.textFile
                selectedTextColor: Colors.textFile
                selectionColor: Colors.selection

                textFormat: TextEdit.PlainText
                renderType: Text.QtRendering
                selectByMouse: true
                antialiasing: true
                background: null
            }

            FontMetrics {
                id: fontMetrics
                font: textArea.font
            }
        }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Effects
import QtQuick.Controls.Basic
import FileSystemModule

pragma ComponentBehavior: Bound

// This is the file system view which gets populated by the C++ model.
Rectangle {
    id: root

    signal fileClicked(string filePath)
    property alias rootIndex: fileSystemTreeView.rootIndex

    TreeView {
        id: fileSystemTreeView

        property int lastIndex: -1

        anchors.fill: parent
        model: FileSystemModel
        rootIndex: FileSystemModel.rootIndex
        boundsBehavior: Flickable.StopAtBounds
        boundsMovement: Flickable.StopAtBounds
        clip: true

        Component.onCompleted: fileSystemTreeView.toggleExpanded(0)

        // The delegate represents a single entry in the filesystem.
        delegate: TreeViewDelegate {
            id: treeDelegate
            indentation: 8
            implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
            implicitHeight: 25

            // Since we have the 'ComponentBehavior Bound' pragma, we need to
            // require these properties from our model. This is a convenient way
            // to bind the properties provided by the model's role names.
            required property int index
            required property url filePath
            required property string fileName

            indicator: Image {
                id: directoryIcon

                x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation)
                anchors.verticalCenter: parent.verticalCenter
                source: treeDelegate.hasChildren ? (treeDelegate.expanded
                            ? "../icons/folder_open.svg" : "../icons/folder_closed.svg")
                        : "../icons/generic_file.svg"
                sourceSize.width: 20
                sourceSize.height: 20
                fillMode: Image.PreserveAspectFit

                smooth: true
                antialiasing: true
                asynchronous: true
            }

            contentItem: Text {
                text: treeDelegate.fileName
                color: Colors.text
            }

            background: Rectangle {
                color: (treeDelegate.index === fileSystemTreeView.lastIndex)
                    ? Colors.selection
                    : (hoverHandler.hovered ? Colors.active : "transparent")
            }

            // We color the directory icons with this MultiEffect, where we overlay
            // the colorization color ontop of the SVG icons.
            MultiEffect {
                id: iconOverlay

                anchors.fill: directoryIcon
                source: directoryIcon
                colorization: 1.0
                brightness: 1.0
                colorizationColor: {
                    const isFile = treeDelegate.index === fileSystemTreeView.lastIndex
                                    && !treeDelegate.hasChildren;
                    if (isFile)
                        return Qt.lighter(Colors.folder, 3)

                    const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren;
                    if (isExpandedFolder)
                        return Colors.color2
                    else
                        return Colors.folder
                }
            }

            HoverHandler {
                id: hoverHandler
            }

            TapHandler {
                acceptedButtons: Qt.LeftButton | Qt.RightButton
                onSingleTapped: (eventPoint, button) => {
                    switch (button) {
                        case Qt.LeftButton:
                            fileSystemTreeView.toggleExpanded(treeDelegate.row)
                            fileSystemTreeView.lastIndex = treeDelegate.index
                            // If this model item doesn't have children, it means it's
                            // representing a file.
                            if (!treeDelegate.hasChildren)
                                root.fileClicked(treeDelegate.filePath)
                        break;
                        case Qt.RightButton:
                            if (treeDelegate.hasChildren)
                                contextMenu.popup();
                        break;
                    }
                }
            }

            MyMenu {
                id: contextMenu
                Action {
                    text: qsTr("Set as root index")
                    onTriggered: {
                        fileSystemTreeView.rootIndex = fileSystemTreeView.index(treeDelegate.row, 0)
                    }
                }
                Action {
                    text: qsTr("Reset root index")
                    onTriggered: fileSystemTreeView.rootIndex = undefined
                }
            }
        }

        // Provide our own custom ScrollIndicator for the TreeView.
        ScrollIndicator.vertical: ScrollIndicator {
            active: true
            implicitWidth: 15

            contentItem: Rectangle {
                implicitWidth: 6
                implicitHeight: 6

                color: Colors.color1
                opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0

                Behavior on opacity {
                    OpacityAnimator {
                        duration: 500
                    }
                }
            }
        }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls.Basic
import FileSystemModule

Menu {
    id: root

    delegate: MenuItem {
        id: menuItem
        contentItem: Item {
            Text {
                anchors.verticalCenter: parent.verticalCenter
                anchors.left: parent.left
                anchors.leftMargin: 5

                text: menuItem.text
                color: enabled ? Colors.text : Colors.disabledText
            }
            Rectangle {
                id: indicator

                anchors.verticalCenter: parent.verticalCenter
                anchors.right: parent.right
                width: 6
                height: parent.height

                visible: menuItem.highlighted
                color: Colors.color2
            }
        }
        background: Rectangle {
            implicitWidth: 210
            implicitHeight: 35
            color: menuItem.highlighted ? Colors.active : "transparent"
        }
    }
    background: Rectangle {
        implicitWidth: 210
        implicitHeight: 35
        color: Colors.surface2
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
import FileSystemModule

// The MenuBar also serves as a controller for our window as we don't use any decorations.
MenuBar {
    id: root

    required property ApplicationWindow dragWindow
    property alias infoText: windowInfo.text

    // Customization of the top level menus inside the MenuBar
    delegate: MenuBarItem {
        id: menuBarItem

        contentItem: Text {
            horizontalAlignment: Text.AlignLeft
            verticalAlignment: Text.AlignVCenter

            text: menuBarItem.text
            font: menuBarItem.font
            elide: Text.ElideRight
            color: menuBarItem.highlighted ? Colors.textFile : Colors.text
            opacity: enabled ? 1.0 : 0.3
        }

        background: Rectangle {
            id: background

            color: menuBarItem.highlighted ? Colors.selection : "transparent"
            Rectangle {
                id: indicator

                width: 0; height: 3
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.bottom: parent.bottom

                color: Colors.color1
                states: State {
                    name: "active"
                    when: menuBarItem.highlighted
                    PropertyChanges {
                        indicator.width: background.width - 2
                    }
                }
                transitions: Transition {
                    NumberAnimation {
                        properties: "width"
                        duration: 175
                    }
                }
            }
        }
    }
    // We use the contentItem property as a place to attach our window decorations. Beneath
    // the usual menu entries within a MenuBar, it includes a centered information text, along
    // with the minimize, maximize, and close buttons.
    contentItem: RowLayout {
        id: windowBar

        Layout.fillWidth: true
        Layout.fillHeight: true

        spacing: root.spacing
        Repeater {
            id: menuBarItems

            Layout.alignment: Qt.AlignLeft
            model: root.contentModel
        }

        Item {
            Layout.fillWidth: true
            Layout.fillHeight: true
            Text {
                id: windowInfo

                width: parent.width; height: parent.height
                horizontalAlignment: Text.AlignHCenter
                verticalAlignment: Text.AlignVCenter
                leftPadding: windowActions.width
                color: Colors.text
                clip: true
            }
        }

        RowLayout {
            id: windowActions

            Layout.alignment: Qt.AlignRight
            Layout.fillHeight: true

            spacing: 0

            component InteractionButton: Rectangle {
                id: interactionButton

                signal action()
                property alias hovered: hoverHandler.hovered

                Layout.fillHeight: true
                Layout.preferredWidth: height

                color: hovered ? Colors.background : "transparent"
                HoverHandler {
                    id: hoverHandler
                }
                TapHandler {
                    id: tapHandler
                    onTapped: interactionButton.action()
                }
            }

            InteractionButton {
                id: minimize

                onAction: root.dragWindow.showMinimized()
                Rectangle {
                    anchors.centerIn: parent
                    color: parent.hovered ? Colors.iconIndicator : Colors.icon
                    height: 2
                    width: parent.height - 14
                }
            }

            InteractionButton {
                id: maximize

                onAction: root.dragWindow.showMaximized()
                Rectangle {
                    anchors.fill: parent
                    anchors.margins: 7
                    border.color: parent.hovered ? Colors.iconIndicator : Colors.icon
                    border.width: 2
                    color: "transparent"
                }
            }

            InteractionButton {
                id: close

                color: hovered ? "#ec4143" : "transparent"
                onAction: root.dragWindow.close()
                Rectangle {
                    anchors.centerIn: parent
                    width: parent.height - 8; height: 2

                    rotation: 45
                    antialiasing: true
                    transformOrigin: Item.Center
                    color: parent.hovered ? Colors.iconIndicator : Colors.icon

                    Rectangle {
                        anchors.centerIn: parent
                        width: parent.height
                        height: parent.width

                        antialiasing: true
                        color: parent.color
                    }
                }
            }
        }
    }

    background: Rectangle {
        color: Colors.surface2
        // Make the empty space drag the specified root window.
        WindowDragHandler {
            dragWindow: root.dragWindow
        }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick.Controls
import FileSystemModule

Button {
    required property ApplicationWindow resizeWindow

    icon.width: 20; icon.height: 20
    anchors.right: parent.right
    anchors.bottom: parent.bottom
    rightPadding: 3
    bottomPadding: 3

    icon.source: "../icons/resize.svg"
    icon.color: hovered ? Colors.iconIndicator : Colors.icon

    background: null
    checkable: false
    display: AbstractButton.IconOnly
    onPressed: resizeWindow.startSystemResize(Qt.BottomEdge | Qt.RightEdge)
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
import FileSystemModule

Rectangle {
    id: root

    property alias currentTabIndex: topBar.currentIndex
    required property ApplicationWindow dragWindow
    readonly property int tabBarSpacing: 10

    color: Colors.surface2

    component SidebarEntry: Button {
        id: sidebarButton

        Layout.alignment: Qt.AlignHCenter
        Layout.fillWidth: true

        icon.color: down || checked ? Colors.iconIndicator : Colors.icon
        icon.width: 27
        icon.height: 27

        topPadding: 0
        rightPadding: 0
        bottomPadding: 0
        leftPadding: 0
        background: null

        Rectangle {
            id: indicator

            anchors.verticalCenter: parent.verticalCenter
            x: 2
            width: 4
            height: sidebarButton.icon.height * 1.2

            visible: sidebarButton.checked
            color: Colors.color1
        }
    }

    // TabBar is designed to be horizontal, whereas we need a vertical bar.
    // We can easily achieve that by using a Container.
    component TabBar: Container {
        id: tabBarComponent

        Layout.fillWidth: true
        // ButtonGroup ensures that only one button can be checked at a time.
        ButtonGroup {
            buttons: tabBarComponent.contentChildren

            // We have to manage the currentIndex ourselves, which we do by setting it to the index
            // of the currently checked button. We use setCurrentIndex instead of setting the
            // currentIndex property to avoid breaking bindings. See "Managing the Current Index"
            // in Container's documentation for more information.
            onCheckedButtonChanged: tabBarComponent.setCurrentIndex(
                Math.max(0, buttons.indexOf(checkedButton)))
        }

        contentItem: ColumnLayout {
            spacing: tabBarComponent.spacing
            Repeater {
                model: tabBarComponent.contentModel
            }
        }
    }

    ColumnLayout {
        anchors.fill: root
        anchors.topMargin: root.tabBarSpacing
        anchors.bottomMargin: root.tabBarSpacing

        spacing: root.tabBarSpacing
        TabBar {
            id: topBar

            spacing: root.tabBarSpacing
            // Shows help text when clicked.
            SidebarEntry {
                id: infoTab
                icon.source: "../icons/light_bulb.svg"
                checkable: true
                checked: true
            }

            // Shows the file system when clicked.
            SidebarEntry {
                id: filesystemTab

                icon.source: "../icons/read.svg"
                checkable: true
            }
        }

        // This item acts as a spacer to expand between the checkable and non-checkable buttons.
        Item {
            Layout.fillHeight: true
            Layout.fillWidth: true

            // Make the empty space drag our main window.
            WindowDragHandler {
                dragWindow: root.dragWindow
            }
        }

        TabBar {
            id: bottomBar

            spacing: root.tabBarSpacing
            // Opens the Qt website in the system's web browser.
            SidebarEntry {
                id: qtWebsiteButton
                icon.source: "../icons/globe.svg"
                checkable: false
                onClicked: Qt.openUrlExternally("https://www.qt.io/")
            }

            // Opens the About Qt Window.
            SidebarEntry {
                id: aboutQtButton

                icon.source: "../icons/info_sign.svg"
                checkable: false
                onClicked: aboutQtWindow.visible = !aboutQtWindow.visible
            }
        }
    }

    About {
        id: aboutQtWindow
        visible: false
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls

// Allows dragging the window when placed on an unused section of the UI.
DragHandler {

    required property ApplicationWindow dragWindow

    target: null
    onActiveChanged: {
        if (active) dragWindow.startSystemMove()
    }
}