diff options
Diffstat (limited to 'examples/quickcontrols')
34 files changed, 1336 insertions, 452 deletions
diff --git a/examples/quickcontrols/contactslist/Contact/ContactDelegate.ui.qml b/examples/quickcontrols/contactslist/Contact/ContactDelegate.ui.qml new file mode 100644 index 000000000..affcccc3e --- /dev/null +++ b/examples/quickcontrols/contactslist/Contact/ContactDelegate.ui.qml @@ -0,0 +1,82 @@ +// 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 + +ItemDelegate { + id: delegate + + checkable: true + + contentItem: ColumnLayout { + spacing: 10 + + Label { + text: fullName + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + } + + GridLayout { + id: grid + visible: false + + columns: 2 + rowSpacing: 10 + columnSpacing: 10 + + Label { + text: qsTr("Address:") + Layout.leftMargin: 60 + } + + Label { + text: address + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + } + + Label { + text: qsTr("City:") + Layout.leftMargin: 60 + } + + Label { + text: city + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + } + + Label { + text: qsTr("Number:") + Layout.leftMargin: 60 + } + + Label { + text: number + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + + states: [ + State { + name: "expanded" + when: delegate.checked + + PropertyChanges { + // TODO: When Qt Design Studio supports generalized grouped properties, change to: + // grid.visible: true + target: grid + visible: true + } + } + ] +} diff --git a/examples/quickcontrols/contactslist/Contact/ContactDialog.qml b/examples/quickcontrols/contactslist/Contact/ContactDialog.qml new file mode 100644 index 000000000..d906f00e6 --- /dev/null +++ b/examples/quickcontrols/contactslist/Contact/ContactDialog.qml @@ -0,0 +1,45 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +Dialog { + id: dialog + + signal finished(string fullName, string address, string city, string number) + + function createContact() { + form.fullName.clear(); + form.address.clear(); + form.city.clear(); + form.number.clear(); + + dialog.title = qsTr("Add Contact"); + dialog.open(); + } + + function editContact(contact) { + form.fullName.text = contact.fullName; + form.address.text = contact.address; + form.city.text = contact.city; + form.number.text = contact.number; + + dialog.title = qsTr("Edit Contact"); + dialog.open(); + } + + x: parent.width / 2 - width / 2 + y: parent.height / 2 - height / 2 + + focus: true + modal: true + title: qsTr("Add Contact") + standardButtons: Dialog.Ok | Dialog.Cancel + + contentItem: ContactForm { + id: form + } + + onAccepted: finished(form.fullName.text, form.address.text, form.city.text, form.number.text) +} diff --git a/examples/quickcontrols/contactslist/Contact/ContactForm.ui.qml b/examples/quickcontrols/contactslist/Contact/ContactForm.ui.qml new file mode 100644 index 000000000..56c918619 --- /dev/null +++ b/examples/quickcontrols/contactslist/Contact/ContactForm.ui.qml @@ -0,0 +1,72 @@ +// 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 + +GridLayout { + id: grid + property alias fullName: fullName + property alias address: address + property alias city: city + property alias number: number + property int minimumInputSize: 120 + property string placeholderText: qsTr("<enter>") + + rows: 4 + columns: 2 + + Label { + text: qsTr("Full Name") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: fullName + focus: true + Layout.fillWidth: true + Layout.minimumWidth: grid.minimumInputSize + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + placeholderText: grid.placeholderText + } + + Label { + text: qsTr("Address") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: address + Layout.fillWidth: true + Layout.minimumWidth: grid.minimumInputSize + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + placeholderText: grid.placeholderText + } + + Label { + text: qsTr("City") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: city + Layout.fillWidth: true + Layout.minimumWidth: grid.minimumInputSize + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + placeholderText: grid.placeholderText + } + + Label { + text: qsTr("Number") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: number + Layout.fillWidth: true + Layout.minimumWidth: grid.minimumInputSize + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + placeholderText: grid.placeholderText + } +} diff --git a/examples/quickcontrols/contactslist/Contact/ContactList.qml b/examples/quickcontrols/contactslist/Contact/ContactList.qml new file mode 100644 index 000000000..0b7af32b5 --- /dev/null +++ b/examples/quickcontrols/contactslist/Contact/ContactList.qml @@ -0,0 +1,70 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ApplicationWindow { + id: window + + property int currentContact: -1 + + width: 320 + height: 480 + visible: true + title: qsTr("Contact List") + + ContactDialog { + id: contactDialog + onFinished: function(fullName, address, city, number) { + if (currentContact == -1) + contactView.model.append(fullName, address, city, number) + else + contactView.model.set(currentContact, fullName, address, city, number) + } + } + + Menu { + id: contactMenu + x: parent.width / 2 - width / 2 + y: parent.height / 2 - height / 2 + modal: true + + Label { + padding: 10 + font.bold: true + width: parent.width + horizontalAlignment: Qt.AlignHCenter + text: currentContact >= 0 ? contactView.model.get(currentContact).fullName : "" + } + MenuItem { + text: qsTr("Edit...") + onTriggered: contactDialog.editContact(contactView.model.get(currentContact)) + } + MenuItem { + text: qsTr("Remove") + onTriggered: contactView.model.remove(currentContact) + } + } + + ContactView { + id: contactView + anchors.fill: parent + onPressAndHold: { + currentContact = index + contactMenu.open() + } + } + + RoundButton { + text: qsTr("+") + highlighted: true + anchors.margins: 10 + anchors.right: parent.right + anchors.bottom: parent.bottom + onClicked: { + currentContact = -1 + contactDialog.createContact() + } + } +} diff --git a/examples/quickcontrols/contactslist/Contact/ContactView.ui.qml b/examples/quickcontrols/contactslist/Contact/ContactView.ui.qml new file mode 100644 index 000000000..3b82b681e --- /dev/null +++ b/examples/quickcontrols/contactslist/Contact/ContactView.ui.qml @@ -0,0 +1,36 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import Backend + +ListView { + id: listView + + signal pressAndHold(int index) + + width: 320 + height: 480 + + focus: true + boundsBehavior: Flickable.StopAtBounds + + section.property: "fullName" + section.criteria: ViewSection.FirstCharacter + section.delegate: SectionDelegate { + width: listView.width + } + + delegate: ContactDelegate { + id: delegate + width: listView.width + onPressAndHold: listView.pressAndHold(index) + } + + model: ContactModel { + id: contactModel + } + + ScrollBar.vertical: ScrollBar { } +} diff --git a/examples/quickcontrols/contactslist/Contact/SectionDelegate.ui.qml b/examples/quickcontrols/contactslist/Contact/SectionDelegate.ui.qml new file mode 100644 index 000000000..3a62409a8 --- /dev/null +++ b/examples/quickcontrols/contactslist/Contact/SectionDelegate.ui.qml @@ -0,0 +1,17 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls + +ToolBar { + id: background + + Label { + id: label + text: section + anchors.fill: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + } +} diff --git a/examples/quickcontrols/contactslist/Contact/qmldir b/examples/quickcontrols/contactslist/Contact/qmldir new file mode 100644 index 000000000..339d45a1d --- /dev/null +++ b/examples/quickcontrols/contactslist/Contact/qmldir @@ -0,0 +1,7 @@ +module Contact +ContactList 1.0 ContactList.qml +ContactDialog 1.0 ContactDialog.qml +ContactDelegate 1.0 ContactDelegate.ui.qml +ContactForm 1.0 ContactForm.ui.qml +ContactView 1.0 ContactView.ui.qml +SectionDelegate 1.0 SectionDelegate.ui.qml diff --git a/examples/quickcontrols/contactslist/contactlist.pyproject b/examples/quickcontrols/contactslist/contactlist.pyproject new file mode 100644 index 000000000..75b0bd693 --- /dev/null +++ b/examples/quickcontrols/contactslist/contactlist.pyproject @@ -0,0 +1,10 @@ +{ + "files": ["main.py", + "contactmodel.py", + "Contact/ContactDialog.qml", + "Contact/ContactDelegate.ui.qml", + "Contact/ContactForm.ui.qml", + "Contact/ContactList.qml", + "Contact/ContactView.ui.qml", + "Contact/SectionDelegate.ui.qml"] +} diff --git a/examples/quickcontrols/contactslist/contactmodel.py b/examples/quickcontrols/contactslist/contactmodel.py new file mode 100644 index 000000000..5d2746c2e --- /dev/null +++ b/examples/quickcontrols/contactslist/contactmodel.py @@ -0,0 +1,116 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import bisect +from dataclasses import dataclass +from enum import IntEnum + +from PySide6.QtCore import (QAbstractListModel, QEnum, Qt, QModelIndex, Slot, + QByteArray) +from PySide6.QtQml import QmlElement + +QML_IMPORT_NAME = "Backend" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class ContactModel(QAbstractListModel): + + @QEnum + class ContactRole(IntEnum): + FullNameRole = Qt.DisplayRole + AddressRole = Qt.UserRole + CityRole = Qt.UserRole + 1 + NumberRole = Qt.UserRole + 2 + + @dataclass + class Contact: + fullName: str + address: str + city: str + number: str + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.m_contacts = [] + self.m_contacts.append(self.Contact("Angel Hogan", "Chapel St. 368 ", "Clearwater", + "0311 1823993")) + self.m_contacts.append(self.Contact("Felicia Patton", "Annadale Lane 2", "Knoxville", + "0368 1244494")) + self.m_contacts.append(self.Contact("Grant Crawford", "Windsor Drive 34", "Riverdale", + "0351 7826892")) + self.m_contacts.append(self.Contact("Gretchen Little", "Sunset Drive 348", "Virginia Beach", + "0343 1234991")) + self.m_contacts.append(self.Contact("Geoffrey Richards", "University Lane 54", "Trussville", + "0423 2144944")) + self.m_contacts.append(self.Contact("Henrietta Chavez", "Via Volto San Luca 3", + "Piobesi Torinese", "0399 2826994")) + self.m_contacts.append(self.Contact("Harvey Chandler", "North Squaw Creek 11", + "Madisonville", "0343 1244492")) + self.m_contacts.append(self.Contact("Miguel Gomez", "Wild Rose Street 13", "Trussville", + "0343 9826996")) + self.m_contacts.append(self.Contact("Norma Rodriguez", " Glen Eagles Street 53", + "Buffalo", "0241 5826596")) + self.m_contacts.append(self.Contact("Shelia Ramirez", "East Miller Ave 68", "Pickerington", + "0346 4844556")) + self.m_contacts.append(self.Contact("Stephanie Moss", "Piazza Trieste e Trento 77", + "Roata Chiusani", "0363 0510490")) + + def rowCount(self, parent=QModelIndex()): + return len(self.m_contacts) + + def data(self, index: QModelIndex, role: int): + row = index.row() + if row < self.rowCount(): + if role == ContactModel.ContactRole.FullNameRole: + return self.m_contacts[row].fullName + elif role == ContactModel.ContactRole.AddressRole: + return self.m_contacts[row].address + elif role == ContactModel.ContactRole.CityRole: + return self.m_contacts[row].city + elif role == ContactModel.ContactRole.NumberRole: + return self.m_contacts[row].number + + def roleNames(self): + default = super().roleNames() + default[ContactModel.ContactRole.FullNameRole] = QByteArray(b"fullName") + default[ContactModel.ContactRole.AddressRole] = QByteArray(b"address") + default[ContactModel.ContactRole.CityRole] = QByteArray(b"city") + default[ContactModel.ContactRole.NumberRole] = QByteArray(b"number") + return default + + @Slot(int) + def get(self, row: int): + contact = self.m_contacts[row] + return {"fullName": contact.fullName, "address": contact.address, + "city": contact.city, "number": contact.number} + + @Slot(str, str, str, str) + def append(self, full_name: str, address: str, city: str, number: str): + contact = self.Contact(full_name, address, city, number) + contact_names = [contact.fullName for contact in self.m_contacts] + index = bisect.bisect(contact_names, contact.fullName) + self.beginInsertRows(QModelIndex(), index, index) + self.m_contacts.insert(index, contact) + self.endInsertRows() + + @Slot(int, str, str, str, str) + def set(self, row: int, full_name: str, address: str, city: str, number: str): + if row < 0 or row >= len(self.m_contacts): + return + + self.m_contacts[row] = self.Contact(full_name, address, city, number) + self.dataChanged(self.index(row, 0), self.index(row, 0), + [ContactModel.ContactRole.FullNameRole, + ContactModel.ContactRole.AddressRole, + ContactModel.ContactRole.CityRole, + ContactModel.ContactRole.NumberRole]) + + @Slot(int) + def remove(self, row): + if row < 0 or row >= len(self.m_contacts): + return + + self.beginRemoveRows(QModelIndex(), row, row) + del self.m_contacts[row] + self.endRemoveRows() diff --git a/examples/quickcontrols/contactslist/doc/contactslist.rst b/examples/quickcontrols/contactslist/doc/contactslist.rst new file mode 100644 index 000000000..b5540e39b --- /dev/null +++ b/examples/quickcontrols/contactslist/doc/contactslist.rst @@ -0,0 +1,15 @@ +Qt Quick Controls - Contact List +================================ + +.. tags:: Android + +A QML app using Qt Quick Controls and a Python class that implements a simple +contact list. This example can also be deployed to Android using +**pyside6-android-deploy** + +A PySide6 application that demonstrates the analogous example in Qt +`ContactsList <https://doc.qt.io/qt-6.6/qtquickcontrols-contactlist-example.html>`_ + +.. image:: qtquickcontrols-contactlist.png + :width: 400 + :alt: ContactList Screenshot diff --git a/examples/quickcontrols/contactslist/doc/qtquickcontrols-contactlist.png b/examples/quickcontrols/contactslist/doc/qtquickcontrols-contactlist.png Binary files differnew file mode 100644 index 000000000..9f1c30654 --- /dev/null +++ b/examples/quickcontrols/contactslist/doc/qtquickcontrols-contactlist.png diff --git a/examples/quickcontrols/contactslist/main.py b/examples/quickcontrols/contactslist/main.py new file mode 100644 index 000000000..d501bbdb4 --- /dev/null +++ b/examples/quickcontrols/contactslist/main.py @@ -0,0 +1,28 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +""" +PySide6 port of Qt Quick Controls Contact List example from Qt v6.x +""" +import sys +from pathlib import Path +from PySide6.QtGui import QGuiApplication +from PySide6.QtQml import QQmlApplicationEngine + +from contactmodel import ContactModel # noqa: F401 + +if __name__ == '__main__': + app = QGuiApplication(sys.argv) + app.setOrganizationName("QtProject") + app.setApplicationName("ContactsList") + engine = QQmlApplicationEngine() + + engine.addImportPath(Path(__file__).parent) + engine.loadFromModule("Contact", "ContactList") + + if not engine.rootObjects(): + sys.exit(-1) + + ex = app.exec() + del engine + sys.exit(ex) diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml index 4dfc1590d..7f7798ed8 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/Main.qml @@ -6,46 +6,68 @@ 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("Qt Quick Controls - File System Explorer") + title: qsTr("File System Explorer Example") - property string currentFilePath: "" - property bool expandPath: false + 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 { - rootWindow: root - - infoText: currentFilePath - ? (expandPath ? currentFilePath - : currentFilePath.substring(currentFilePath.lastIndexOf("/") + 1, currentFilePath.length)) - : "File System Explorer" - + dragWindow: root + infoText: root.getInfoText() MyMenu { title: qsTr("File") Action { text: qsTr("Increase Font") - shortcut: "Ctrl++" - onTriggered: textArea.font.pixelSize += 1 + shortcut: StandardKey.ZoomIn + onTriggered: editor.text.font.pixelSize += 1 } Action { text: qsTr("Decrease Font") - shortcut: "Ctrl+-" - onTriggered: textArea.font.pixelSize -= 1 + shortcut: StandardKey.ZoomOut + onTriggered: editor.text.font.pixelSize -= 1 } Action { - text: expandPath ? qsTr("Toggle Short Path") : qsTr("Toggle Expand Path") - enabled: currentFilePath - onTriggered: expandPath = !expandPath + 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 } } @@ -55,134 +77,109 @@ ApplicationWindow { Action { text: qsTr("Cut") shortcut: StandardKey.Cut - enabled: textArea.selectedText.length > 0 - onTriggered: textArea.cut() + enabled: editor.text.selectedText.length > 0 + onTriggered: editor.text.cut() } Action { text: qsTr("Copy") shortcut: StandardKey.Copy - enabled: textArea.selectedText.length > 0 - onTriggered: textArea.copy() + enabled: editor.text.selectedText.length > 0 + onTriggered: editor.text.copy() } Action { text: qsTr("Paste") shortcut: StandardKey.Paste - enabled: textArea.canPaste - onTriggered: textArea.paste() + enabled: editor.text.canPaste + onTriggered: editor.text.paste() } Action { text: qsTr("Select All") shortcut: StandardKey.SelectAll - enabled: textArea.length > 0 - onTriggered: textArea.selectAll() + enabled: editor.text.length > 0 + onTriggered: editor.text.selectAll() } Action { text: qsTr("Undo") shortcut: StandardKey.Undo - enabled: textArea.canUndo - onTriggered: textArea.undo() + enabled: editor.text.canUndo + onTriggered: editor.text.undo() } } } - - Rectangle { + // Set up the layout of the main components in a row: + // [ Sidebar, Navigation, Editor ] + RowLayout { anchors.fill: parent - color: Colors.background - - RowLayout { - anchors.fill: parent - spacing: 0 - - // Stores the buttons that navigate the application. - Sidebar { - id: sidebar - rootWindow: root - - Layout.preferredWidth: 60 - Layout.fillHeight: true - } + 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 - - handle: Rectangle { - implicitWidth: 10 - color: SplitHandle.pressed ? Colors.color2 : Colors.background - border.color: Colors.color2 - opacity: SplitHandle.hovered || SplitHandle.pressed ? 1.0 : 0.0 - - Behavior on opacity { - OpacityAnimator { - duration: 900 - } + // 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 } } + } - // We use an inline component to make a reusable TextArea component. - // This is convenient when the component is only used in one file. - component MyTextArea: TextArea { - antialiasing: true - color: Colors.textFile - selectedTextColor: Colors.textFile - selectionColor: Colors.selection - renderType: Text.QtRendering - textFormat: TextEdit.PlainText - - background: null - } - - Rectangle { - color: Colors.surface1 - - SplitView.preferredWidth: 250 - SplitView.fillHeight: true - - StackLayout { - currentIndex: sidebar.currentTabIndex - - anchors.fill: parent - - // Shows the help text. - MyTextArea { - readOnly: true - 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 - } - - // Shows the files on the file system. - FileSystemView { - id: fileSystemView - color: Colors.surface1 - - onFileClicked: (path) => root.currentFilePath = path - } + 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 } - } - - // The ScrollView that contains the TextArea which shows the file's content. - ScrollView { - leftPadding: 20 - topPadding: 20 - bottomPadding: 20 - clip: true - - SplitView.fillWidth: true - SplitView.fillHeight: true - - property alias textArea: textArea - MyTextArea { - id: textArea - text: FileSystemModel.readFile(root.currentFilePath) + // 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 {} + } + + ResizeButton { + resizeWindow: root } } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/app.qrc b/examples/quickcontrols/filesystemexplorer/FileSystemModule/app.qrc index 05fc728e7..fec76fe67 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/app.qrc +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/app.qrc @@ -3,9 +3,9 @@ <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/Icon.qml</file> <file>qml/MyMenu.qml</file> <file>qml/MyMenuBar.qml</file> <file>qml/ResizeButton.qml</file> diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/icons.qrc b/examples/quickcontrols/filesystemexplorer/FileSystemModule/icons.qrc index 5793a62cf..97d8a3d79 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/icons.qrc +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/icons.qrc @@ -1,5 +1,6 @@ <RCC> <qresource> + <file>icons/app_icon.svg</file> <file>icons/folder_closed.svg</file> <file>icons/folder_open.svg</file> <file>icons/generic_file.svg</file> diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/icons/app_icon.svg b/examples/quickcontrols/filesystemexplorer/FileSystemModule/icons/app_icon.svg new file mode 100644 index 000000000..5aae4221f --- /dev/null +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/icons/app_icon.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#EBDBB2" d="M13.25 8.5a.75.75 0 1 1-.75-.75.75.75 0 0 1 .75.75zM9.911 21.35l.816.578C10.819 21.798 13 18.666 13 13h-1a15.503 15.503 0 0 1-2.089 8.35zM4 6.703V10a2.002 2.002 0 0 1-2 2v1a2.002 2.002 0 0 1 2 2v3.297A3.707 3.707 0 0 0 7.703 22H9v-1H7.703A2.706 2.706 0 0 1 5 18.297V15a2.999 2.999 0 0 0-1.344-2.5A2.999 2.999 0 0 0 5 10V6.703A2.706 2.706 0 0 1 7.703 4H9V3H7.703A3.707 3.707 0 0 0 4 6.703zM20 10V6.703A3.707 3.707 0 0 0 16.297 3H15v1h1.297A2.706 2.706 0 0 1 19 6.703V10a2.999 2.999 0 0 0 1.344 2.5A2.999 2.999 0 0 0 19 15v3.297A2.706 2.706 0 0 1 16.297 21H15v1h1.297A3.707 3.707 0 0 0 20 18.297V15a2.002 2.002 0 0 1 2-2v-1a2.002 2.002 0 0 1-2-2z"/><path fill="none" d="M0 0h24v24H0z"/></svg> diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml index b7bc0ac6f..178bf03e4 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/About.qml @@ -7,53 +7,87 @@ import FileSystemModule ApplicationWindow { id: root - width: 500 - height: 360 + width: 650 + height: 550 flags: Qt.Window | Qt.FramelessWindowHint color: Colors.surface1 menuBar: MyMenuBar { id: menuBar - implicitHeight: 20 - rootWindow: root + + 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: Qt.size(80, 80) + sourceSize.width: 80 + sourceSize.height: 80 fillMode: Image.PreserveAspectFit + smooth: true antialiasing: true asynchronous: true } - TextArea { - anchors.top: logo.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 20 - antialiasing: true - wrapMode: Text.WrapAnywhere - color: Colors.textFile - horizontalAlignment: Text.AlignHCenter - readOnly: true - selectionColor: Colors.selection - text: qsTr("Qt Group (Nasdaq Helsinki: QTCOM) is a global software company with a strong \ -presence in more than 70 industries and is the leading independent technology behind 1+ billion \ -devices and applications. Qt is used by major global companies and developers worldwide, and the \ -technology enables its customers to deliver exceptional user experiences and advance their digital \ -transformation initiatives. Qt achieves this through its cross-platform software framework for the \ -development of apps and devices, under both commercial and open-source licenses.") - background: Rectangle { - color: "transparent" - } + 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 } - ResizeButton {} } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Colors.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Colors.qml index 280f89286..285667773 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Colors.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Colors.qml @@ -1,22 +1,23 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -pragma Singleton import QtQuick +pragma Singleton + QtObject { - readonly property color background: "#23272E" - readonly property color surface1: "#1E2227" + readonly property color background: "#292828" + readonly property color surface1: "#171819" readonly property color surface2: "#090A0C" - readonly property color text: "#ABB2BF" - readonly property color textFile: "#C5CAD3" - readonly property color disabledText: "#454D5F" - readonly property color selection: "#2C313A" - readonly property color active: "#23272E" - readonly property color inactive: "#3E4452" - readonly property color folder: "#3D4451" - readonly property color icon: "#3D4451" - readonly property color iconIndicator: "#E5C07B" - readonly property color color1: "#E06B74" - readonly property color color2: "#62AEEF" + 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" } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Editor.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Editor.qml new file mode 100644 index 000000000..80f7c04c5 --- /dev/null +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Editor.qml @@ -0,0 +1,160 @@ +// 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 + } + } + } +} diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/FileSystemView.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/FileSystemView.qml index ade2e48c1..db955168c 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/FileSystemView.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/FileSystemView.qml @@ -2,26 +2,31 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick -import QtQuick.Layouts +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 - property int lastIndex: -1 - Component.onCompleted: fileSystemTreeView.toggleExpanded(0) // The delegate represents a single entry in the filesystem. @@ -31,50 +36,101 @@ Rectangle { 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: null - - contentItem: Item { - anchors.fill: parent + indicator: Image { + id: directoryIcon - Icon { - id: directoryIcon - x: leftMargin + (depth * indentation) - anchors.verticalCenter: parent.verticalCenter - path: treeDelegate.hasChildren - ? (treeDelegate.expanded ? "../icons/folder_open.svg" : "../icons/folder_closed.svg") + 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" - iconColor: (treeDelegate.expanded && treeDelegate.hasChildren) ? Colors.color2 : Colors.folder - } - Text { - anchors.left: directoryIcon.right - anchors.verticalCenter: parent.verticalCenter - width: parent.width - text: model.fileName - color: Colors.text - } + 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 + color: (treeDelegate.index === fileSystemTreeView.lastIndex) ? Colors.selection : (hoverHandler.hovered ? Colors.active : "transparent") } - TapHandler { - onSingleTapped: { - fileSystemTreeView.toggleExpanded(row) - fileSystemTreeView.lastIndex = index - // If this model item doesn't have children, it means it's representing a file. - if (!treeDelegate.hasChildren) - root.fileClicked(filePath) + // 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. @@ -85,6 +141,7 @@ Rectangle { contentItem: Rectangle { implicitWidth: 6 implicitHeight: 6 + color: Colors.color1 opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Icon.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Icon.qml deleted file mode 100644 index 25162d9d3..000000000 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Icon.qml +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause - -import QtQuick -import QtQuick.Effects - -// Custom Component for displaying Icons -Item { - id: root - - required property url path - property real padding: 5 - property real size: 30 - property alias iconColor: overlay.colorizationColor - property alias hovered: mouse.hovered - - width: size - height: size - - Image { - id: icon - anchors.fill: root - anchors.margins: padding - source: path - sourceSize: Qt.size(size, size) - fillMode: Image.PreserveAspectFit - smooth: true - antialiasing: true - asynchronous: true - } - - MultiEffect { - id: overlay - anchors.fill: icon - source: icon - colorization: 1.0 - brightness: 1.0 - } - - HoverHandler { - id: mouse - acceptedDevices: PointerDevice.Mouse - } -} diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenu.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenu.qml index 99795b5e5..1f1d30c56 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenu.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenu.qml @@ -8,35 +8,38 @@ import FileSystemModule Menu { id: root - background: Rectangle { - implicitWidth: 200 - implicitHeight: 40 - color: Colors.surface2 - } - delegate: MenuItem { id: menuItem - implicitWidth: 200 - implicitHeight: 40 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 + } } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenuBar.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenuBar.qml index a2a3fea88..4874a2c03 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenuBar.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/MyMenuBar.qml @@ -6,130 +6,172 @@ 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. +// The MenuBar also serves as a controller for our window as we don't use any decorations. MenuBar { id: root - required property ApplicationWindow rootWindow + required property ApplicationWindow dragWindow property alias infoText: windowInfo.text - implicitHeight: 25 - - // The top level menus on the left side + // Customization of the top level menus inside the MenuBar delegate: MenuBarItem { id: menuBarItem - implicitHeight: 25 contentItem: Text { horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter - color: menuBarItem.highlighted ? Colors.textFile : Colors.text - opacity: enabled ? 1.0 : 0.3 + text: menuBarItem.text - elide: Text.ElideRight 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 + color: Colors.color1 states: State { - name: "active"; when: menuBarItem.highlighted - PropertyChanges { target: indicator; width: parent.width } + name: "active" + when: menuBarItem.highlighted + PropertyChanges { + indicator.width: background.width - 2 + } } - transitions: Transition { NumberAnimation { properties: "width" - duration: 300 + 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 - // The background property contains an information text in the middle as well as the - // Minimize, Maximize and Close Buttons. - background: Rectangle { - color: Colors.surface2 - // Make the empty space drag the specified root window. - WindowDragHandler { dragWindow: rootWindow } + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: root.spacing + Repeater { + id: menuBarItems - Text { - id: windowInfo - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - color: Colors.text + Layout.alignment: Qt.AlignLeft + model: root.contentModel } - component InteractionButton: Rectangle { - signal action; - property alias hovered: hoverHandler.hovered + 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 + } + } - width: root.height - anchors.top: parent.top - anchors.bottom: parent.bottom - color: hovered ? Colors.background : "transparent" + RowLayout { + id: windowActions - HoverHandler { id: hoverHandler } - TapHandler { onTapped: action() } - } + Layout.alignment: Qt.AlignRight + Layout.fillHeight: true - InteractionButton { - id: minimize + spacing: 0 - anchors.right: maximize.left - onAction: rootWindow.showMinimized() - Rectangle { - width: parent.height - 10; height: 2 - anchors.centerIn: parent - color: parent.hovered ? Colors.iconIndicator : Colors.icon + 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: maximize + InteractionButton { + id: minimize - anchors.right: close.left - onAction: rootWindow.showMaximized() - Rectangle { - anchors.fill: parent - anchors.margins: 5 - border.width: 2 - color: "transparent" - border.color: parent.hovered ? Colors.iconIndicator : Colors.icon + onAction: root.dragWindow.showMinimized() + Rectangle { + anchors.centerIn: parent + color: parent.hovered ? Colors.iconIndicator : Colors.icon + height: 2 + width: parent.height - 14 + } } - } - InteractionButton { - id: close + InteractionButton { + id: maximize - color: hovered ? "#ec4143" : "transparent" - anchors.right: parent.right - onAction: rootWindow.close() - Rectangle { - width: parent.height - 8; height: 2 - anchors.centerIn: parent - color: parent.hovered ? Colors.iconIndicator : Colors.icon - rotation: 45 - transformOrigin: Item.Center - antialiasing: true + 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 { - width: parent.height - height: parent.width anchors.centerIn: parent - color: parent.color + 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 + } + } } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml index eb2e5bc02..0df65bf82 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/ResizeButton.qml @@ -5,6 +5,8 @@ import QtQuick.Controls import FileSystemModule Button { + required property ApplicationWindow resizeWindow + icon.width: 20; icon.height: 20 anchors.right: parent.right anchors.bottom: parent.bottom @@ -12,12 +14,10 @@ Button { bottomPadding: 3 icon.source: "../icons/resize.svg" - icon.color: down || checked ? Colors.iconIndicator : Colors.icon + icon.color: hovered ? Colors.iconIndicator : Colors.icon + background: null checkable: false display: AbstractButton.IconOnly - background: null - onPressed: { - root.startSystemResize(Qt.BottomEdge | Qt.RightEdge) - } + onPressed: resizeWindow.startSystemResize(Qt.BottomEdge | Qt.RightEdge) } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml index 9d08562d9..aac530394 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qml/Sidebar.qml @@ -8,77 +8,92 @@ import FileSystemModule Rectangle { id: root + + property alias currentTabIndex: topBar.currentIndex + required property ApplicationWindow dragWindow + readonly property int tabBarSpacing: 10 + color: Colors.surface2 - required property ApplicationWindow rootWindow - property alias currentTabIndex: tabBar.currentIndex + component SidebarEntry: Button { + id: sidebarButton - ColumnLayout { - anchors.fill: root - anchors.topMargin: 10 - anchors.bottomMargin: 10 - spacing: 10 + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true - // TabBar is designed to be horizontal, whereas we need a vertical bar. - // We can easily achieve that by using a Container. - Container { - id: tabBar + icon.color: down || checked ? Colors.iconIndicator : Colors.icon + icon.width: 27 + icon.height: 27 - Layout.fillWidth: true + topPadding: 0 + rightPadding: 0 + bottomPadding: 0 + leftPadding: 0 + background: null - // ButtonGroup ensures that only one button can be checked at a time. - ButtonGroup { - buttons: tabBar.contentItem.children - // 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: tabBar.setCurrentIndex(Math.max(0, buttons.indexOf(checkedButton))) - } + Rectangle { + id: indicator - contentItem: ColumnLayout { - spacing: tabBar.spacing + anchors.verticalCenter: parent.verticalCenter + x: 2 + width: 4 + height: sidebarButton.icon.height * 1.2 - Repeater { - model: tabBar.contentModel - } - } + 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))) + } - component SidebarEntry: Button { - id: sidebarButton - icon.color: down || checked ? Colors.iconIndicator : Colors.icon - icon.width: 35 - icon.height: 35 - leftPadding: 8 + indicator.width - - background: null - - Rectangle { - id: indicator - x: 4 - anchors.verticalCenter: parent.verticalCenter - width: 4 - height: sidebarButton.icon.width - color: Colors.color1 - visible: sidebarButton.checked - } + 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 - - Layout.alignment: Qt.AlignHCenter } // Shows the file system when clicked. SidebarEntry { + id: filesystemTab + icon.source: "../icons/read.svg" checkable: true - - Layout.alignment: Qt.AlignHCenter } } @@ -88,25 +103,31 @@ Rectangle { Layout.fillWidth: true // Make the empty space drag our main window. - WindowDragHandler { dragWindow: rootWindow } + WindowDragHandler { + dragWindow: root.dragWindow + } } - // Opens the Qt website in the system's web browser. - SidebarEntry { - id: qtWebsiteButton - icon.source: "../icons/globe.svg" - checkable: false + TabBar { + id: bottomBar - onClicked: Qt.openUrlExternally("https://www.qt.io/") - } + 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 + // Opens the About Qt Window. + SidebarEntry { + id: aboutQtButton - onClicked: aboutQtWindow.visible = !aboutQtWindow.visible + icon.source: "../icons/info_sign.svg" + checkable: false + onClicked: aboutQtWindow.visible = !aboutQtWindow.visible + } } } diff --git a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir index ff7ecb757..b1f684600 100644 --- a/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir +++ b/examples/quickcontrols/filesystemexplorer/FileSystemModule/qmldir @@ -1,7 +1,7 @@ module FileSystemModule Main 1.0 Main.qml -Icon 1.0 qml/Icon.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 diff --git a/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.rst b/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.rst index 0260928b6..b46cbec79 100644 --- a/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.rst +++ b/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.rst @@ -37,3 +37,16 @@ pleasing UIs. .. image:: filesystemexplorer.webp :target: filesystemexplorer.webp :alt: QtQuickControls Filesystem Explorer Screenshot + +References +---------- + +If you're interested in the C++ version of this example, you can find it +`here <https://doc-snapshots.qt.io/qt6-dev/qtquickcontrols-filesystemexplorer-example.html>`_. + +Additionally, there is a detailed +`tutorial <https://doc.qt.io/qtforpython-6/tutorials/extendedexplorer/extendedexplorer.html>`_ +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. diff --git a/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.webp b/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.webp Binary files differindex cce7e1daf..10ad0d26e 100644 --- a/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.webp +++ b/examples/quickcontrols/filesystemexplorer/doc/filesystemexplorer.webp diff --git a/examples/quickcontrols/filesystemexplorer/editormodels.py b/examples/quickcontrols/filesystemexplorer/editormodels.py new file mode 100644 index 000000000..688147726 --- /dev/null +++ b/examples/quickcontrols/filesystemexplorer/editormodels.py @@ -0,0 +1,116 @@ +# 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() diff --git a/examples/quickcontrols/filesystemexplorer/filesystemexplorer.py b/examples/quickcontrols/filesystemexplorer/filesystemexplorer.py deleted file mode 100644 index 90579b360..000000000 --- a/examples/quickcontrols/filesystemexplorer/filesystemexplorer.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2023 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 PySide6.QtWidgets import QFileSystemModel -from PySide6.QtGui import QGuiApplication -from PySide6.QtQml import (QQmlApplicationEngine, QmlElement, QmlSingleton) -from PySide6.QtCore import (Slot, QFile, QTextStream, QMimeDatabase, QFileInfo, QStandardPaths) - -import sys - - -QML_IMPORT_NAME = "FileSystemModule" -QML_IMPORT_MAJOR_VERSION = 1 - - -@QmlElement -@QmlSingleton -class FileSystemModel(QFileSystemModel): - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setRootPath(QStandardPaths.writableLocation(QStandardPaths.HomeLocation)) - self.db = QMimeDatabase() - - # we only need one column in this example - def columnCount(self, parent): - return 1 - - # 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.db.mimeTypeForFile(QFileInfo(file)) - if 'text' in mime.comment().lower() or any('text' in s.lower() for s in mime.parentMimeTypes()): - if file.open(QFile.ReadOnly | QFile.Text): - stream = QTextStream(file).readAll() - return stream - else: - return self.tr("Error opening the file!") - return self.tr("File type not supported!") - - -if __name__ == '__main__': - app = QGuiApplication(sys.argv) - app.setOrganizationName("QtProject") - app.setApplicationName("File System Explorer") - 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) - - sys.exit(app.exec()) diff --git a/examples/quickcontrols/filesystemexplorer/filesystemexplorer.pyproject b/examples/quickcontrols/filesystemexplorer/filesystemexplorer.pyproject index 1e1aa2ad8..8053cfab0 100644 --- a/examples/quickcontrols/filesystemexplorer/filesystemexplorer.pyproject +++ b/examples/quickcontrols/filesystemexplorer/filesystemexplorer.pyproject @@ -1,19 +1,21 @@ { "files": [ - "filesystemexplorer.py", + "main.py", + "editormodels.py", "FileSystemModule/qmldir", "FileSystemModule/app.qrc", "FileSystemModule/qmldir", "FileSystemModule/Main.qml", "FileSystemModule/qml/About.qml", "FileSystemModule/qml/Colors.qml", + "FileSystemModule/qml/Editor.qml", "FileSystemModule/qml/FileSystemView.qml", - "FileSystemModule/qml/Icon.qml", "FileSystemModule/qml/MyMenu.qml", "FileSystemModule/qml/MyMenuBar.qml", "FileSystemModule/qml/ResizeButton.qml", "FileSystemModule/qml/Sidebar.qml", "FileSystemModule/qml/WindowDragHandler.qml", + "FileSystemModule/icons/app_icon.svg", "FileSystemModule/icons/folder_closed.svg", "FileSystemModule/icons/folder_open.svg", "FileSystemModule/icons/generic_file.svg", diff --git a/examples/quickcontrols/filesystemexplorer/main.py b/examples/quickcontrols/filesystemexplorer/main.py new file mode 100644 index 000000000..8fad951cb --- /dev/null +++ b/examples/quickcontrols/filesystemexplorer/main.py @@ -0,0 +1,48 @@ +# 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(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()) diff --git a/examples/quickcontrols/gallery/doc/gallery.rst b/examples/quickcontrols/gallery/doc/gallery.rst index 29cd49f14..acd5096f6 100644 --- a/examples/quickcontrols/gallery/doc/gallery.rst +++ b/examples/quickcontrols/gallery/doc/gallery.rst @@ -1,6 +1,8 @@ Qt Quick Controls 2 - Gallery ============================= +.. tags:: Android + The gallery example is a simple application with a drawer menu that contains all the Qt Quick Controls 2. Each menu item opens a page that shows the graphical appearance of a control, allows you to interact with the control, and diff --git a/examples/quickcontrols/gallery/gallery.py b/examples/quickcontrols/gallery/gallery.py index 1cdc30bab..6c2a3612e 100644 --- a/examples/quickcontrols/gallery/gallery.py +++ b/examples/quickcontrols/gallery/gallery.py @@ -17,7 +17,7 @@ from PySide6.QtCore import QSettings, QUrl from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQuickControls2 import QQuickStyle -import rc_gallery +import rc_gallery # noqa: F401 if __name__ == "__main__": QGuiApplication.setApplicationName("Gallery") |