Qt Quick Controls - Filesystem Explorer¶
This example demonstrates how to create a modern-looking filesystem explorer with a dark-themed user interface that has a uniform look across all operating systems. Custom Qt Quick Controls have been implemented to provide a clean and intuitive UI for opening and navigating text-files from the filesystem.
Frameless Window¶
To maximize the available space, we use a frameless window. The basic
functionality, such as minimizing, maximizing, and closing the window,
has been moved to a customized MenuBar called MyMenuBar
. Users can
drag the window thanks to the WindowDragHandler
added to the Sidebar
and MenuBar.
Customization¶
Combining customized animations and colors with QtQuick Controls allows us to easily create custom user interfaces. This example showcases the potential of QtQuick Controls for creating aesthetically pleasing UIs.
With the knowledge gained from this example, developers can apply similar techniques to create their own customized UIs using PySide’s QtQuick Controls.
References¶
If you’re interested in the C++ version of this example, you can find it here.
Additionally, there is a detailed tutorial available that provides step-by-step instructions on how to extend this example with additional features. This tutorial can be helpful if you want to explore and learn more about building upon the existing functionality of the filesystem explorer.
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
"""
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(sys.path[0] + "/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 __future__ import annotations
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 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()
}
}