diff --git a/Cargo.lock b/Cargo.lock index 3574567..55532cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + [[package]] name = "cc" version = "1.2.16" @@ -19,13 +25,19 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clang-format" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "696283b40e1a39d208ee614b92e5f6521d16962edeb47c48372585ec92419943" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -62,6 +74,7 @@ dependencies = [ "cxx-qt", "cxx-qt-build", "cxx-qt-lib", + "dirs", "log", ] @@ -121,7 +134,7 @@ dependencies = [ "cxx-qt-macro", "qt-build-utils", "static_assertions", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -210,6 +223,27 @@ dependencies = [ "syn", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "either" version = "1.15.0" @@ -222,6 +256,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "indoc" version = "2.0.6" @@ -258,6 +303,16 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "link-cplusplus" version = "1.0.10" @@ -295,6 +350,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "proc-macro2" version = "1.0.94" @@ -311,7 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb239fdd8c036fabb95364320041ef68197cd4ab971bb3b4ca3ea0b7b93d12c" dependencies = [ "cc", - "thiserror", + "thiserror 1.0.69", "versions", ] @@ -324,6 +385,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.12", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -412,7 +484,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -426,6 +507,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -460,6 +552,12 @@ dependencies = [ "nom", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi-util" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 8b7c81c..c039058 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ cxx = "1.0.95" cxx-qt = "0.7" cxx-qt-lib = { version="0.7", features = ["qt_full"] } log = { version = "0.4.27", features = [] } +dirs = "6.0.0" [build-dependencies] # The link_qt_object_files feature is required for statically linking Qt 6. diff --git a/README.md b/README.md index 6172640..0caa5c2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,25 @@ Using Qt Assistant is recommended. It comes with Qt6 when installed. Run the fol nohup $HOME/Qt/6.8.3/gcc_64/bin/assistant > /dev/null 2>&1 & ``` +If you are looking for an include path from Qt + +```bash +find /usr/include/x86_64-linux-gnu/qt6/ -name QFile* + +/usr/include/x86_64-linux-gnu/qt6/QtWidgets/QFileIconProvider +/usr/include/x86_64-linux-gnu/qt6/QtWidgets/QFileDialog +/usr/include/x86_64-linux-gnu/qt6/QtGui/QFileSystemModel +/usr/include/x86_64-linux-gnu/qt6/QtGui/QFileOpenEvent +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFile +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileDevice +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileSystemWatcher +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileInfoList +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileInfo +/usr/include/x86_64-linux-gnu/qt6/QtCore/QFileSelector +``` + +This helped find that QFileSystemModel is in QtGui and not QtCore. + ### Resources Some helpful links for reading up on QML if you're just getting started. diff --git a/build.rs b/build.rs index 07367f3..69a103e 100644 --- a/build.rs +++ b/build.rs @@ -14,6 +14,7 @@ fn main() { qml_files: &[ "qml/main.qml", "qml/ClideAboutWindow.qml", + "qml/ClideTreeView.qml", "qml/ClideProjectView.qml", "qml/ClideEditor.qml", "qml/ClideMenuBar.qml", diff --git a/icons/folder_closed.svg b/icons/folder_closed.svg new file mode 100644 index 0000000..281be32 --- /dev/null +++ b/icons/folder_closed.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/icons/folder_open.svg b/icons/folder_open.svg new file mode 100644 index 0000000..09f7615 --- /dev/null +++ b/icons/folder_open.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/icons/generic_file.svg b/icons/generic_file.svg new file mode 100644 index 0000000..e0423f2 --- /dev/null +++ b/icons/generic_file.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/qml/ClideProjectView.qml b/qml/ClideProjectView.qml index eead520..569a038 100644 --- a/qml/ClideProjectView.qml +++ b/qml/ClideProjectView.qml @@ -5,6 +5,11 @@ import QtQuick.Layouts import clide.module 1.0 SplitView { + id: root + + // Path to the file selected in the tree view. + property string selectedFilePath; + Layout.fillHeight: true Layout.fillWidth: true anchors.fill: parent @@ -42,12 +47,10 @@ SplitView { wrapMode: TextArea.Wrap } - // TODO: Shows the files on the file system. - // ClideTreeView { - // id: fileSystemView - // color: Colors.surface1 - // onFileClicked: path => root.currentFilePath = path - // } + ClideTreeView { + id: clideTreeView + onFileClicked: path => root.currentFilePath = path + } } } ClideEditor { diff --git a/qml/ClideTreeView.qml b/qml/ClideTreeView.qml new file mode 100644 index 0000000..a2cf773 --- /dev/null +++ b/qml/ClideTreeView.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Controls + +import clide.module 1.0 + +Rectangle { + id: root + signal fileClicked(string filePath) + + TreeView { + id: fileSystemTreeView + + // rootIndex: FileSystem.rootIndex + property int lastIndex: -1 + + // model: FileSystem + anchors.fill: parent + 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: RustColors.editor_text + } + + background: Rectangle { + color: (treeDelegate.index === fileSystemTreeView.lastIndex) + ? RustColors.editor_highlight + : (hoverHandler.hovered ? RustColors.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(RustColors.explorer_folder, 3) + // + // const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren; + // if (isExpandedFolder) + // return RustColors.explorer_forder_open + // else + // return RustColors.explorer_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; + } + } + } + + Menu { + 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: RustColors.scrollbar + opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 + + Behavior on opacity { + OpacityAnimator { + duration: 500 + } + } + } + } + } +} diff --git a/src/colors.rs b/src/colors.rs index 1b699b5..7d1f08f 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -26,6 +26,8 @@ pub mod qobject { #[qproperty(QColor, editor_highlight)] #[qproperty(QColor, gutter)] #[qproperty(QColor, explorer_background)] + #[qproperty(QColor, explorer_folder)] + #[qproperty(QColor, explorer_folder_open)] type RustColors = super::RustColorsImpl; } } @@ -50,6 +52,8 @@ pub struct RustColorsImpl { editor_highlight: QColor, gutter: QColor, explorer_background: QColor, + explorer_folder: QColor, + explorer_folder_open: QColor, } impl Default for RustColorsImpl { @@ -72,6 +76,8 @@ impl Default for RustColorsImpl { editor_highlight: QColor::try_from("#ccced3").unwrap(), gutter: QColor::try_from("#1e1f22").unwrap(), explorer_background: QColor::try_from("#3c3f41").unwrap(), + explorer_folder: QColor::try_from("#FFF").unwrap(), + explorer_folder_open: QColor::try_from("#FFF").unwrap(), } } } diff --git a/src/filesystem.rs b/src/filesystem.rs index ecc9fcd..0f97344 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -14,19 +14,31 @@ pub mod qobject { #[qml_element] #[qml_singleton] #[qproperty(QString, file_path, cxx_name = "filePath")] + #[qproperty(QModelIndex, root_index, cxx_name = "rootIndex")] type FileSystem = super::FileSystemImpl; + #[qinvokable] + #[cxx_name = "columnCount"] + pub fn column_count(self: &FileSystem, index: &QModelIndex) -> i32; + #[qinvokable] #[cxx_name = "readFile"] fn read_file(self: &FileSystem, path: &QString) -> QString; + + // TODO: Remove if unused in QML. + #[qinvokable] + #[cxx_name = "setInitialDirectory"] + fn set_initial_directory(self: &FileSystem, path: &QString); } } use cxx_qt_lib::{QModelIndex, QString}; +use dirs; use std::fs; + pub struct FileSystemImpl { file_path: QString, - model_index: QModelIndex, + root_index: QModelIndex, } // Default is explicit to make the editor open this source file initially. @@ -34,7 +46,7 @@ impl Default for FileSystemImpl { fn default() -> Self { Self { file_path: QString::from(file!()), - model_index: Default::default(), + root_index: Default::default(), } } } @@ -50,4 +62,23 @@ impl qobject::FileSystem { .expect(format!("Failed to read file {}", path).as_str()), ) } + + // There will never be more than one column. + pub fn column_count(&self, _index: &QModelIndex) -> i32 { + 1 + } + + fn set_initial_directory(&self, path: &QString) { + if !path.is_empty() + && fs::metadata(path.to_string()) + .expect(format!("Failed to get metadata for file {}", path).as_str()) + .is_file() + { + // Open the file + // setRootPa + } else { + // If the initial directory can't be opened, attempt to find the home directory. + // dirs::home_dir() + } + } }