Compare commits
2 Commits
main
...
325cf285fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 325cf285fc | |||
| aa8590cd5c |
@@ -1,6 +1,3 @@
|
||||
|
||||
[build]
|
||||
rustflags = [ "-C", "link-arg=-fuse-ld=lld", ]
|
||||
|
||||
[env]
|
||||
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
|
||||
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"
|
||||
|
||||
33
.qmllint.ini
33
.qmllint.ini
@@ -1,33 +0,0 @@
|
||||
[General]
|
||||
DisableDefaultImports=false
|
||||
|
||||
[Warnings]
|
||||
AccessSingletonViaObject=warning
|
||||
AttachedPropertyReuse=disable
|
||||
BadSignalHandlerParameters=warning
|
||||
CompilerWarnings=disable
|
||||
Deprecated=warning
|
||||
DuplicatePropertyBinding=warning
|
||||
DuplicatedName=warning
|
||||
ImportFailure=warning
|
||||
IncompatibleType=warning
|
||||
InheritanceCycle=warning
|
||||
InvalidLintDirective=warning
|
||||
LintPluginWarnings=disable
|
||||
MissingProperty=warning
|
||||
MissingType=warning
|
||||
MultilineStrings=info
|
||||
NonListProperty=warning
|
||||
PrefixedImportType=warning
|
||||
PropertyAliasCycles=warning
|
||||
ReadOnlyProperty=warning
|
||||
RequiredProperty=warning
|
||||
RestrictedType=warning
|
||||
TopLevelComponent=warning
|
||||
UncreatableType=warning
|
||||
UnqualifiedAccess=warning
|
||||
UnresolvedType=warning
|
||||
UnusedImports=info
|
||||
UseProperFunction=warning
|
||||
VarUsedBeforeDeclaration=warning
|
||||
WithStatement=warning
|
||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -295,7 +295,6 @@ dependencies = [
|
||||
"cxx-qt",
|
||||
"cxx-qt-build",
|
||||
"cxx-qt-lib",
|
||||
"devicons",
|
||||
"dirs",
|
||||
"edtui",
|
||||
"log",
|
||||
@@ -668,15 +667,6 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "devicons"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830e47e2f330cf4fdd5a958dcef921b9523ffc21ab6713aa5e77ba2cce03904b"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
||||
@@ -18,7 +18,6 @@ tui-logger = "0.18.1"
|
||||
edtui = "0.11.1"
|
||||
strum = "0.27.2"
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
devicons = "0.6.12"
|
||||
|
||||
[build-dependencies]
|
||||
# The link_qt_object_files feature is required for statically linking Qt 6.
|
||||
|
||||
28
README.md
28
README.md
@@ -19,31 +19,6 @@ And of course, [Rust](https://www.rust-lang.org/tools/install).
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
This project requires at least Qt 6.7.3 To check your Qt version
|
||||
|
||||
```bash
|
||||
qmake6 -query QT_VERSION
|
||||
```
|
||||
|
||||
Use the [Qt Installer](https://www.qt.io/development/download) to download and install the Qt version of your choice.
|
||||
If the installer is run with `sudo`, the default install location is `/opt/Qt`, otherwise Qt will be installed into your home directory.
|
||||
|
||||
**You must set the QMAKE variable before building clide**. This should be a path to `qmake6` binary installed on your system.
|
||||
The following export is the default installation path for Qt 6.7 on Ubuntu 24.04
|
||||
|
||||
```bash
|
||||
export QMAKE=$HOME/Qt/6.7.3/gcc_64/bin/qmake6
|
||||
export LD_LIBRARY_PATH=$HOME/Qt/6.7.3/gcc_64/lib
|
||||
```
|
||||
|
||||
Though environment variables set using `export` will take precedence, these can also be set in [.cargo/config.toml](./.cargo/config.toml) for conveinence
|
||||
|
||||
```toml
|
||||
[env]
|
||||
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
|
||||
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To install and run clide
|
||||
@@ -107,8 +82,6 @@ cargo run
|
||||
clide
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Development
|
||||
|
||||
It's recommended to use RustRover or Qt Creator for development.
|
||||
@@ -153,7 +126,6 @@ Some helpful links for reading up on QML if you're just getting started.
|
||||
* [All QML Controls Types](https://doc.qt.io/qt-6/qtquick-controls-qmlmodule.html)
|
||||
* [KDAB CXX-Qt Book](https://kdab.github.io/cxx-qt/book/)
|
||||
* [github.com/KDAB/cxx-qt](https://github.com/KDAB/cxx-qt)
|
||||
* [QML and C++ Intergration](https://doc.qt.io/qt-6/qtqml-cppintegration-overview.html)
|
||||
|
||||
|
||||
### Plugins
|
||||
|
||||
20
build.rs
20
build.rs
@@ -2,21 +2,12 @@ use cxx_qt_build::{CxxQtBuilder, QmlModule};
|
||||
|
||||
fn main() {
|
||||
CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[
|
||||
"qml/ClideApplicationView.qml",
|
||||
"qml/ClideEditorView.qml",
|
||||
"qml/ClideExplorerView.qml",
|
||||
"qml/ClideTreeView.qml",
|
||||
"qml/Components/ClideAboutWindow.qml",
|
||||
"qml/Components/ClideBreadCrumbs.qml",
|
||||
"qml/Components/ClideEditor.qml",
|
||||
"qml/Components/ClideHandle.qml",
|
||||
"qml/Components/ClideLogger.qml",
|
||||
"qml/Components/ClideMenu.qml",
|
||||
"qml/Components/ClideMenuBar.qml",
|
||||
"qml/Components/ClideMenuItem.qml",
|
||||
"qml/Components/ClideScrollBar.qml",
|
||||
"qml/Logger/Logger.qml",
|
||||
"qml/main.qml",
|
||||
"qml/ClideAboutWindow.qml",
|
||||
"qml/ClideTreeView.qml",
|
||||
"qml/ClideProjectView.qml",
|
||||
"qml/ClideEditor.qml",
|
||||
"qml/ClideMenuBar.qml",
|
||||
]))
|
||||
// Link Qt's Network library
|
||||
// - Qt Core is always linked
|
||||
@@ -27,7 +18,6 @@ fn main() {
|
||||
.qt_module("Gui")
|
||||
.qt_module("Svg")
|
||||
.qt_module("Xml")
|
||||
.qrc("./resources.qrc")
|
||||
.files(["src/gui/colors.rs", "src/gui/filesystem.rs"])
|
||||
.build();
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@@ -6,22 +6,18 @@ import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
|
||||
color: RustColors.gutter
|
||||
// Create the window with no frame and keep it on top.
|
||||
flags: Qt.Tool | Qt.FramelessWindowHint
|
||||
height: 350
|
||||
width: 450
|
||||
visible: root.active
|
||||
height: 350
|
||||
// Create the window with no frame and keep it on top.
|
||||
flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
||||
color: RustColors.gutter
|
||||
|
||||
// Hide the window when it loses focus.
|
||||
onActiveChanged: {
|
||||
Logger.debug("Setting active: " + root.active)
|
||||
if (!root.active) {
|
||||
if (!active) {
|
||||
root.visible = false;
|
||||
}
|
||||
}
|
||||
@@ -31,38 +27,48 @@ ApplicationWindow {
|
||||
id: logo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 20
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 20
|
||||
|
||||
source: "../icons/kilroy-256.png"
|
||||
sourceSize.width: 80
|
||||
sourceSize.height: 80
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
smooth: true
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
source: "qrc:/images/kilroy.png"
|
||||
sourceSize.height: 80
|
||||
sourceSize.width: 80
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 20
|
||||
anchors.right: parent.right
|
||||
anchors.top: logo.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 20
|
||||
|
||||
TextArea {
|
||||
antialiasing: true
|
||||
background: null
|
||||
color: RustColors.editor_text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
readOnly: true
|
||||
selectedTextColor: RustColors.editor_highlighted_text
|
||||
selectionColor: RustColors.editor_highlight
|
||||
text: qsTr("<h3>About CLIDE</h3>" + "<p>A simple text editor written in Rust and QML using CXX-Qt.</p>" + "<p>Personal website <a href=\"http://shaunreed.com\">shaunreed.com</a></p>" + "<p>Project notes <a href=\"http://knoats.com\">knoats.com</a></p>" + "<p>This project is developed at <a href=\"http://git.shaunreed.com/shaunrd0/clide\">git.shaunreed.com</a></p>" + "<p><a href=\"https://github.com/KDAB/cxx-qt\">KDAB CXX-Qt repository</a></p>" + "<p>Copyright (C) 2026 Shaun Reed, all rights reserved.</p>")
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
|
||||
text: qsTr("<h3>About CLIDE</h3>"
|
||||
+ "<p>A simple text editor written in Rust and QML using CXX-Qt.</p>"
|
||||
+ "<p>Personal website <a href=\"http://shaunreed.com\">shaunreed.com</a></p>"
|
||||
+ "<p>Project notes <a href=\"http://knoats.com\">knoats.com</a></p>"
|
||||
+ "<p>This project is developed at <a href=\"http://git.shaunreed.com/shaunrd0/clide\">git.shaunreed.com</a></p>"
|
||||
+ "<p><a href=\"https://github.com/KDAB/cxx-qt\">KDAB CXX-Qt repository</a></p>"
|
||||
+ "<p>Copyright (C) 2026 Shaun Reed, all rights reserved.</p>")
|
||||
color: RustColors.editor_text
|
||||
wrapMode: Text.WordWrap
|
||||
readOnly: true
|
||||
antialiasing: true
|
||||
background: null
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
|
||||
// Path to the directory of the project opened in clide.
|
||||
required property string projectDir
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// Customized handle to drag between the Navigation and the Editor.
|
||||
handle: ClideHandle {
|
||||
hovered: SplitHandle.hovered
|
||||
pressed: SplitHandle.pressed
|
||||
}
|
||||
|
||||
ClideExplorerView {
|
||||
SplitView.fillHeight: true
|
||||
SplitView.preferredWidth: 200
|
||||
projectDir: root.projectDir
|
||||
|
||||
// Open files when clicked in the explorer.
|
||||
onFileClicked: path => {
|
||||
Logger.trace("Setting editor path from ClideExplorerView signal: " + path)
|
||||
clideEditorView.filePath = path;
|
||||
}
|
||||
}
|
||||
ClideEditorView {
|
||||
id: clideEditorView
|
||||
|
||||
SplitView.fillHeight: true
|
||||
SplitView.fillWidth: true
|
||||
// Provide a path to the file currently open in the text editor.
|
||||
// Initialized using the Default trait in Rust QML singleton FileSystem.
|
||||
filePath: FileSystem.filePath
|
||||
}
|
||||
}
|
||||
210
qml/ClideEditor.qml
Normal file
210
qml/ClideEditor.qml
Normal file
@@ -0,0 +1,210 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
orientation: Qt.Vertical
|
||||
|
||||
// The path to the file to show in the text editor.
|
||||
// This is updated by a signal caught within ClideProjectView.
|
||||
// Initialized by the Default trait for the Rust QML singleton FileSystem.
|
||||
required property string filePath
|
||||
|
||||
// Customized handle to drag between the Editor and the Console.
|
||||
handle: Rectangle {
|
||||
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
|
||||
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
|
||||
implicitHeight: 8
|
||||
radius: 2.5
|
||||
|
||||
// Execute these behaviors when the color is changed.
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 400
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
// 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
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: false
|
||||
// Calculating the width correctly is important as the number grows.
|
||||
// We need to ensure space required to show N line number digits.
|
||||
// We use log10 to find how many digits are needed in a line number.
|
||||
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
|
||||
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
|
||||
// The +10 is additional spacing and can be adjusted.
|
||||
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
|
||||
contentY: editorFlickable.contentY
|
||||
interactive: false
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
topPadding: textArea.topPadding
|
||||
|
||||
Repeater {
|
||||
id: repeatedLineNumbers
|
||||
// TODO: Bug where text wrapping shows as new line number.
|
||||
model: textArea.lineCount
|
||||
|
||||
// This Item is used for each line number in the gutter.
|
||||
delegate: Item {
|
||||
// Calculates the height of each line in the text area.
|
||||
height: textArea.contentHeight / textArea.lineCount
|
||||
width: parent.width
|
||||
|
||||
required property int index
|
||||
|
||||
// Show the line number.
|
||||
Label {
|
||||
id: numbers
|
||||
color: RustColors.linenumber
|
||||
font: textArea.font
|
||||
height: parent.height
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: parent.index + 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: parent.width - indicator.width
|
||||
}
|
||||
// Draw edge along the right side of the line number.
|
||||
Rectangle {
|
||||
id: indicator
|
||||
anchors.left: numbers.right
|
||||
color: RustColors.linenumber
|
||||
height: parent.height
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Flickable {
|
||||
id: editorFlickable
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
height: 650
|
||||
|
||||
ScrollBar.horizontal: MyScrollBar {
|
||||
}
|
||||
ScrollBar.vertical: MyScrollBar {
|
||||
}
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: textArea
|
||||
focus: true
|
||||
persistentSelection: true
|
||||
antialiasing: true
|
||||
selectByMouse: true
|
||||
selectionColor: RustColors.editor_highlight
|
||||
selectedTextColor: RustColors.editor_highlighted_text
|
||||
textFormat: Qt.AutoText
|
||||
wrapMode: TextArea.Wrap
|
||||
text: FileSystem.readFile(root.filePath)
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
|
||||
// TODO: Handle saving
|
||||
// Component.onCompleted: {
|
||||
// if (Qt.application.arguments.length === 2)
|
||||
// textDocument.source = "file:" + Qt.application.arguments[1]
|
||||
// else
|
||||
// textDocument.source = "qrc:/texteditor.html"
|
||||
// }
|
||||
// textDocument.onStatusChanged: {
|
||||
// // a message lookup table using computed properties:
|
||||
// // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer
|
||||
// const statusMessages = {
|
||||
// [ TextDocument.ReadError ]: qsTr("Failed to load “%1”"),
|
||||
// [ TextDocument.WriteError ]: qsTr("Failed to save “%1”"),
|
||||
// [ TextDocument.NonLocalFileError ]: qsTr("Not a local file: “%1”"),
|
||||
// }
|
||||
// const err = statusMessages[textDocument.status]
|
||||
// if (err) {
|
||||
// errorDialog.text = err.arg(textDocument.source)
|
||||
// errorDialog.open()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
|
||||
font: textArea.font
|
||||
}
|
||||
}
|
||||
}
|
||||
TextArea {
|
||||
id: areaConsole
|
||||
|
||||
height: 100
|
||||
placeholderText: qsTr("Placeholder for bash terminal.")
|
||||
placeholderTextColor: "white"
|
||||
readOnly: true
|
||||
wrapMode: TextArea.Wrap
|
||||
background: Rectangle {
|
||||
color: RustColors.editor_background
|
||||
implicitHeight: 100
|
||||
// border.color: control.enabled ? RustColors.active : RustColors.inactive
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Scroll bar gutter
|
||||
background: Rectangle {
|
||||
implicitHeight: scrollBar.interactive ? 8 : 4
|
||||
implicitWidth: scrollBar.interactive ? 8 : 4
|
||||
color: RustColors.scrollbar_gutter
|
||||
|
||||
// Fade the scrollbar gutter when inactive.
|
||||
opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.2
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll bar
|
||||
contentItem: Rectangle {
|
||||
implicitHeight: scrollBar.interactive ? 8 : 4
|
||||
implicitWidth: scrollBar.interactive ? 8 : 4
|
||||
|
||||
// If we don't need a scrollbar, fallback to the gutter color.
|
||||
// If the scrollbar is required change it's color based on activity.
|
||||
color: scrollBar.size < 1.0 ? scrollBar.active ? RustColors.scrollbar_active : RustColors.scrollbar : RustColors.scrollbar_gutter
|
||||
// Smooth transition between color changes based on the state above.
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 1000
|
||||
}
|
||||
}
|
||||
// Fade the scrollbar when inactive.
|
||||
opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.35
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// The path to the file to show in the text editor.
|
||||
// This is updated by a signal caught within ClideApplicationView.
|
||||
required property string filePath
|
||||
|
||||
clip: true
|
||||
color: "transparent"
|
||||
radius: 20
|
||||
|
||||
SplitView {
|
||||
anchors.fill: parent
|
||||
orientation: Qt.Vertical
|
||||
spacing: 3
|
||||
|
||||
// Customized handle to drag between the Editor and the Console.
|
||||
handle: ClideHandle {
|
||||
hovered: SplitHandle.hovered
|
||||
pressed: SplitHandle.pressed
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Show logging is working.
|
||||
Logger.info("Info logs");
|
||||
Logger.warn("Warning logs");
|
||||
Logger.debug("Debug logs");
|
||||
Logger.error("Error logs");
|
||||
Logger.trace("Trace logs");
|
||||
}
|
||||
|
||||
ClideEditor {
|
||||
SplitView.preferredHeight: 650
|
||||
}
|
||||
ClideLogger {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property string projectDir
|
||||
|
||||
signal fileClicked(string path)
|
||||
|
||||
clip: true
|
||||
color: RustColors.explorer_background
|
||||
topLeftRadius: 10
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
ClideBreadCrumbs {
|
||||
id: breadCrumb
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 15
|
||||
Layout.rightMargin: 15
|
||||
Layout.topMargin: 10
|
||||
path: clideTreeView.rootDirectory
|
||||
|
||||
onCrumbClicked: path => {
|
||||
Logger.trace("Crumb clicked: " + path);
|
||||
clideTreeView.rootDirectory = path;
|
||||
}
|
||||
onResetRoot: {
|
||||
clideTreeView.rootDirectory = clideTreeView.originalRootDirectory;
|
||||
}
|
||||
}
|
||||
ClideTreeView {
|
||||
id: clideTreeView
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Path to the directory opened in the file explorer.
|
||||
originalRootDirectory: root.projectDir
|
||||
rootDirectory: root.projectDir
|
||||
|
||||
// Pass the signal to the parent component using another signal.
|
||||
onFileClicked: path => root.fileClicked(path)
|
||||
onRootDirectoryChanged: {
|
||||
Logger.log("Setting root directory: " + clideTreeView.rootDirectory);
|
||||
breadCrumb.path = clideTreeView.rootDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
qml/ClideMenuBar.qml
Normal file
190
qml/ClideMenuBar.qml
Normal file
@@ -0,0 +1,190 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
MenuBar {
|
||||
// Base settings for each Menu.
|
||||
component ClideMenu : Menu {
|
||||
background: Rectangle {
|
||||
color: RustColors.menubar
|
||||
implicitWidth: 100
|
||||
radius: 2
|
||||
}
|
||||
}
|
||||
|
||||
// Base settings for each MenuItem.
|
||||
component ClideMenuItem : MenuItem {
|
||||
id: root
|
||||
|
||||
background: Rectangle {
|
||||
color: root.hovered ? RustColors.hovered : RustColors.unhovered
|
||||
radius: 2.5
|
||||
}
|
||||
contentItem: IconLabel {
|
||||
color: "black"
|
||||
font.family: "Helvetica"
|
||||
text: root.text
|
||||
}
|
||||
}
|
||||
|
||||
// Background for this MenuBar.
|
||||
background: Rectangle {
|
||||
color: RustColors.menubar
|
||||
border.color: RustColors.menubar_border
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// File Menu
|
||||
Action {
|
||||
id: actionNewProject
|
||||
|
||||
text: qsTr("&New Project...")
|
||||
}
|
||||
Action {
|
||||
id: actionOpen
|
||||
|
||||
text: qsTr("&Open...")
|
||||
}
|
||||
Action {
|
||||
id: actionSave
|
||||
|
||||
text: qsTr("&Save")
|
||||
}
|
||||
Action {
|
||||
id: actionExit
|
||||
|
||||
text: qsTr("&Exit")
|
||||
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
ClideMenu {
|
||||
title: qsTr("&File")
|
||||
|
||||
ClideMenuItem {
|
||||
action: actionNewProject
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionOpen
|
||||
onTriggered: FileSystem.setDirectory(FileSystem.filePath)
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionSave
|
||||
}
|
||||
MenuSeparator {
|
||||
background: Rectangle {
|
||||
border.color: color
|
||||
color: RustColors.menubar_border
|
||||
implicitHeight: 3
|
||||
implicitWidth: 200
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionExit
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Edit Menu
|
||||
Action {
|
||||
id: actionUndo
|
||||
|
||||
text: qsTr("&Undo")
|
||||
}
|
||||
Action {
|
||||
id: actionRedo
|
||||
|
||||
text: qsTr("&Redo")
|
||||
}
|
||||
Action {
|
||||
id: actionCut
|
||||
|
||||
text: qsTr("&Cut")
|
||||
}
|
||||
Action {
|
||||
id: actionCopy
|
||||
|
||||
text: qsTr("&Copy")
|
||||
}
|
||||
Action {
|
||||
id: actionPaste
|
||||
|
||||
text: qsTr("&Paste")
|
||||
}
|
||||
ClideMenu {
|
||||
title: qsTr("&Edit")
|
||||
|
||||
ClideMenuItem {
|
||||
action: actionUndo
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionRedo
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionCut
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionCopy
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionPaste
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// View Menu
|
||||
Action {
|
||||
id: actionToolWindows
|
||||
|
||||
text: qsTr("&Tool Windows")
|
||||
}
|
||||
Action {
|
||||
id: actionAppearance
|
||||
|
||||
text: qsTr("&Appearance")
|
||||
}
|
||||
ClideMenu {
|
||||
title: qsTr("&View")
|
||||
|
||||
ClideMenuItem {
|
||||
action: actionToolWindows
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionAppearance
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Help Menu
|
||||
ClideAboutWindow {
|
||||
id: clideAbout
|
||||
}
|
||||
|
||||
Action {
|
||||
id: actionDocumentation
|
||||
|
||||
text: qsTr("&Documentation")
|
||||
}
|
||||
Action {
|
||||
id: actionAbout
|
||||
// Toggle the about window with the menu item is clicked.
|
||||
onTriggered: clideAbout.visible = !clideAbout.visible
|
||||
|
||||
text: qsTr("&About")
|
||||
}
|
||||
ClideMenu {
|
||||
title: qsTr("&Help")
|
||||
|
||||
ClideMenuItem {
|
||||
action: actionDocumentation
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: actionAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
64
qml/ClideProjectView.qml
Normal file
64
qml/ClideProjectView.qml
Normal file
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
|
||||
// Path to the directory of the project opened in clide.
|
||||
required property string projectDir
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
anchors.fill: parent
|
||||
|
||||
// Customized handle to drag between the Navigation and the Editor.
|
||||
handle: Rectangle {
|
||||
id: verticalSplitHandle
|
||||
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
|
||||
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
|
||||
implicitWidth: 8
|
||||
radius: 2.5
|
||||
|
||||
// Execute these behaviors when the color is changed.
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 400
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: navigationView
|
||||
color: RustColors.explorer_background
|
||||
|
||||
SplitView.fillHeight: true
|
||||
SplitView.minimumWidth: 0
|
||||
SplitView.preferredWidth: 200
|
||||
SplitView.maximumWidth: 250
|
||||
|
||||
StackLayout {
|
||||
anchors.fill: parent
|
||||
ClideTreeView {
|
||||
id: clideTreeView
|
||||
onFileClicked: path => root.projectDir = path
|
||||
|
||||
// Path to the directory opened in the file explorer.
|
||||
rootDirectory: root.projectDir
|
||||
}
|
||||
}
|
||||
}
|
||||
ClideEditor {
|
||||
SplitView.fillWidth: true
|
||||
|
||||
// Provide a path to the file currently open in the text editor.
|
||||
// Initialized using the Default trait in Rust QML singleton FileSystem.
|
||||
filePath: FileSystem.filePath
|
||||
}
|
||||
}
|
||||
@@ -3,164 +3,158 @@
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
TreeView {
|
||||
Rectangle {
|
||||
id: root
|
||||
color: RustColors.explorer_background
|
||||
|
||||
property int lastIndex: -1
|
||||
required property string originalRootDirectory
|
||||
property string rootDirectory
|
||||
property int rootIndent: 25
|
||||
required property string rootDirectory
|
||||
|
||||
signal fileClicked(string filePath)
|
||||
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.StopAtBounds
|
||||
clip: true
|
||||
// https://doc.qt.io/qt-6/qml-qtquick-treeview.html
|
||||
TreeView {
|
||||
id: fileSystemTreeView
|
||||
anchors.margins: 15
|
||||
|
||||
// The model is implemented in filesystem.rs
|
||||
model: FileSystem
|
||||
// Set the root directory on the Rust model, returning the QModeIndex to use for the root of the tree view widget.
|
||||
rootIndex: FileSystem.setDirectory(root.rootDirectory)
|
||||
property int lastIndex: -1
|
||||
|
||||
// Provide our own custom ScrollIndicator for the TreeView.
|
||||
ScrollBar.horizontal: ClideScrollBar {
|
||||
sizeModifier: 3
|
||||
}
|
||||
ScrollBar.vertical: ClideScrollBar {
|
||||
sizeModifier: 3
|
||||
}
|
||||
|
||||
// The delegate represents a single entry in the filesystem.
|
||||
delegate: TreeViewDelegate {
|
||||
id: treeDelegate
|
||||
|
||||
required property string fileName
|
||||
required property url filePath
|
||||
required property int index
|
||||
|
||||
implicitHeight: 25
|
||||
implicitWidth: root.width
|
||||
indentation: 12
|
||||
|
||||
background: Rectangle {
|
||||
color: current ? RustColors.explorer_folder_open : "transparent"
|
||||
radius: 20
|
||||
width: root.width
|
||||
model: FileSystemSortProxyModel {
|
||||
id: fs
|
||||
}
|
||||
// Item name.
|
||||
contentItem: Text {
|
||||
anchors.left: itemIcon.right
|
||||
anchors.leftMargin: 5
|
||||
color: RustColors.explorer_text
|
||||
text: treeDelegate.fileName
|
||||
}
|
||||
// Item Icon.
|
||||
indicator: Label {
|
||||
id: itemIcon
|
||||
anchors.fill: parent
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.StopAtBounds
|
||||
clip: true
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
antialiasing: true
|
||||
enabled: false
|
||||
focus: false
|
||||
font.family: localFont.font.family
|
||||
font.pixelSize: 18
|
||||
smooth: true
|
||||
// Get the icon from Rust implementation.
|
||||
text: root.model.icon(filePath)
|
||||
x: root.rootIndent + (treeDelegate.depth * treeDelegate.indentation) + (carrotIndicator.visible ? carrotIndicator.width : 0)
|
||||
Component.onCompleted: {
|
||||
fs.setDirectory(root.rootDirectory)
|
||||
fileSystemTreeView.expandRecursively(0, -1)
|
||||
}
|
||||
|
||||
// Directory carrot indicator.
|
||||
Label {
|
||||
id: carrotIndicator
|
||||
// The delegate represents a single entry in the filesystem.
|
||||
delegate: TreeViewDelegate {
|
||||
id: treeDelegate
|
||||
indentation: 8
|
||||
implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
|
||||
implicitHeight: 25
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.family: localFont.font.family
|
||||
font.pixelSize: 10
|
||||
font.weight: localFont.font.weight
|
||||
text: expanded ? "⮟" : "⮞"
|
||||
visible: isTreeNode && hasChildren
|
||||
x: (root.rootIndent - implicitWidth) + (depth * indentation)
|
||||
}
|
||||
// Apply colorization effects to the icon for the item.
|
||||
MultiEffect {
|
||||
anchors.fill: itemIcon
|
||||
brightness: 1.0
|
||||
colorization: 1.0
|
||||
colorizationColor: {
|
||||
const isFile = !treeDelegate.hasChildren;
|
||||
if (isFile)
|
||||
return Qt.lighter(RustColors.explorer_folder, 2);
|
||||
const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren;
|
||||
if (isExpandedFolder)
|
||||
return Qt.darker(RustColors.explorer_folder, 2);
|
||||
else
|
||||
return RustColors.explorer_folder;
|
||||
}
|
||||
source: itemIcon
|
||||
}
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
required property int index
|
||||
required property url filePath
|
||||
required property string fileName
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
indicator: Image {
|
||||
id: directoryIcon
|
||||
|
||||
onSingleTapped: (eventPoint, button) => {
|
||||
switch (button) {
|
||||
case Qt.LeftButton:
|
||||
function setSourceImage() {
|
||||
let folderOpen = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z\"/></svg>";
|
||||
let folderClosed = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z\"/></svg>";
|
||||
let file = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z\"/></svg>";
|
||||
// If the item has children, it's a directory.
|
||||
if (treeDelegate.hasChildren) {
|
||||
root.toggleExpanded(treeDelegate.row);
|
||||
return treeDelegate.expanded ?
|
||||
folderOpen : folderClosed;
|
||||
} else {
|
||||
// If this model item doesn't have children, it means it's representing a file.
|
||||
root.fileClicked(treeDelegate.filePath);
|
||||
return file
|
||||
}
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
contextMenu.popup();
|
||||
break;
|
||||
}
|
||||
|
||||
x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: setSourceImage()
|
||||
sourceSize.width: 15
|
||||
sourceSize.height: 15
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
smooth: true
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
ClideMenu {
|
||||
id: contextMenu
|
||||
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
enabled: treeDelegate.hasChildren
|
||||
text: qsTr("Set root")
|
||||
contentItem: Text {
|
||||
text: treeDelegate.fileName
|
||||
color: RustColors.explorer_text
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
Logger.debug("Setting new root directory: " + treeDelegate.filePath);
|
||||
root.rootDirectory = treeDelegate.filePath;
|
||||
background: Rectangle {
|
||||
// TODO: Fix flickering from color transition on states here.
|
||||
color: (treeDelegate.index === fileSystemTreeView.lastIndex)
|
||||
? RustColors.explorer_text_selected
|
||||
: (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent")
|
||||
radius: 2.5
|
||||
opacity: hoverHandler.hovered ? 0.75 : 1.0
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
text: qsTr("Reset root")
|
||||
|
||||
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: {
|
||||
Logger.log("Resetting root directory: " + root.originalRootDirectory);
|
||||
root.rootDirectory = root.originalRootDirectory;
|
||||
console.log("Setting directory: " + treeDelegate.filePath)
|
||||
FileSystemSortProxyModel.setDirectory(treeDelegate.filePath)
|
||||
}
|
||||
}
|
||||
Action {
|
||||
text: qsTr("Reset root index")
|
||||
onTriggered: {
|
||||
FileSystemSortProxyModel.setDirectory("")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selectionModel: ItemSelectionModel {
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: localFont
|
||||
// Provide our own custom ScrollIndicator for the TreeView.
|
||||
ScrollIndicator.vertical: ScrollIndicator {
|
||||
active: true
|
||||
implicitWidth: 15
|
||||
|
||||
source: "qrc:/fonts/saucecodepro-xlight.ttf"
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 6
|
||||
implicitHeight: 6
|
||||
|
||||
color: RustColors.scrollbar
|
||||
opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var fullPaths: []
|
||||
required property string path
|
||||
property var segments: []
|
||||
|
||||
signal crumbClicked(string path)
|
||||
signal resetRoot
|
||||
|
||||
function rebuildSegments(): string {
|
||||
let cleaned = path;
|
||||
if (cleaned.endsWith("/"))
|
||||
cleaned = cleaned.slice(0, -1);
|
||||
Logger.trace("Building segments for path: " + cleaned);
|
||||
segments = ["/"];
|
||||
fullPaths = ["/"];
|
||||
let parts = cleaned.split("/");
|
||||
let current = "";
|
||||
// We already pushed the root `/` path during initialization, so skip index 0.
|
||||
for (let i = 1; i < parts.length; ++i) {
|
||||
current += "/" + parts[i];
|
||||
Logger.trace("Pushing path: " + parts[i] + " Current: " + current);
|
||||
segments.push(parts[i]);
|
||||
fullPaths.push(current);
|
||||
}
|
||||
// Update the model used in the Repeater to show the new segments.
|
||||
repeater.model = segments;
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
implicitHeight: breadcrumbRow.implicitHeight
|
||||
width: parent.width
|
||||
|
||||
Component.onCompleted: rebuildSegments()
|
||||
onPathChanged: rebuildSegments()
|
||||
|
||||
Flow {
|
||||
id: breadcrumbRow
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 2
|
||||
width: parent.width
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: root.segments
|
||||
|
||||
delegate: Text {
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
function getText(): string {
|
||||
if (modelData === "/") {
|
||||
return modelData;
|
||||
}
|
||||
return modelData + "/";
|
||||
}
|
||||
|
||||
// Show blue underlined hyperlink text if the mouse is hovering a segment.
|
||||
color: mouseArea.containsMouse ? "#2a7fff" : RustColors.explorer_text
|
||||
font.underline: mouseArea.containsMouse
|
||||
text: getText()
|
||||
|
||||
// Click events for each path segment call signal so the parent can set the file explorer root path.
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
Logger.info(index + "] Breadcrumb clicked:" + root.fullPaths[index]);
|
||||
crumbClicked(root.fullPaths[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onSingleTapped: contextMenu.popup()
|
||||
}
|
||||
ClideMenu {
|
||||
id: contextMenu
|
||||
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
text: qsTr("Reset root")
|
||||
|
||||
onTriggered: {
|
||||
Logger.info("Resetting root directory from ClideBreadCrumbs");
|
||||
resetRoot();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
Rectangle {
|
||||
color: RustColors.editor_background
|
||||
|
||||
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
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: false
|
||||
// Calculating the width correctly is important as the number grows.
|
||||
// We need to ensure space required to show N line number digits.
|
||||
// We use log10 to find how many digits are needed in a line number.
|
||||
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
|
||||
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
|
||||
// The +10 is additional spacing and can be adjusted.
|
||||
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
|
||||
contentY: editorFlickable.contentY
|
||||
interactive: false
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
topPadding: textArea.topPadding
|
||||
|
||||
Repeater {
|
||||
id: repeatedLineNumbers
|
||||
|
||||
// TODO: Bug where text wrapping shows as new line number.
|
||||
model: textArea.lineCount
|
||||
|
||||
// This Item is used for each line number in the gutter.
|
||||
delegate: Item {
|
||||
required property int index
|
||||
|
||||
// Calculates the height of each line in the text area.
|
||||
height: textArea.contentHeight / textArea.lineCount
|
||||
width: parent.width
|
||||
|
||||
// Show the line number.
|
||||
Label {
|
||||
id: numbers
|
||||
|
||||
color: RustColors.linenumber
|
||||
font: textArea.font
|
||||
height: parent.height
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: parent.index + 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: parent.width - indicator.width
|
||||
}
|
||||
// Draw an edge along the right side of the line number.
|
||||
Rectangle {
|
||||
id: indicator
|
||||
|
||||
anchors.left: numbers.right
|
||||
color: RustColors.linenumber
|
||||
height: parent.height
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Flickable {
|
||||
id: editorFlickable
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
height: 650
|
||||
|
||||
ScrollBar.horizontal: ClideScrollBar {
|
||||
}
|
||||
ScrollBar.vertical: ClideScrollBar {
|
||||
}
|
||||
TextArea.flickable: TextArea {
|
||||
id: textArea
|
||||
|
||||
antialiasing: true
|
||||
focus: true
|
||||
persistentSelection: true
|
||||
selectByMouse: true
|
||||
selectedTextColor: RustColors.editor_highlighted_text
|
||||
selectionColor: RustColors.editor_highlight
|
||||
text: FileSystem.readFile(root.filePath)
|
||||
textFormat: Qt.AutoText
|
||||
wrapMode: TextArea.Wrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
|
||||
font: textArea.font
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
readonly property color currentColor: {
|
||||
if (pressed) {
|
||||
return RustColors.pressed;
|
||||
} else if (hovered) {
|
||||
return RustColors.hovered;
|
||||
} else {
|
||||
return "transparent";
|
||||
}
|
||||
}
|
||||
required property bool hovered
|
||||
required property bool pressed
|
||||
|
||||
border.color: currentColor
|
||||
color: currentColor
|
||||
implicitHeight: 8
|
||||
implicitWidth: 8
|
||||
radius: 2.5
|
||||
opacity: root.hovered ? 1.0 : 0.0
|
||||
|
||||
// Execute these behaviors when the color is changed.
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import clide.module 1.0
|
||||
import Logger 1.0
|
||||
|
||||
Rectangle {
|
||||
color: RustColors.terminal_background
|
||||
radius: 10
|
||||
|
||||
ListModel {
|
||||
id: model
|
||||
|
||||
}
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
function getLogColor(level) {
|
||||
switch (level) {
|
||||
case "INFO":
|
||||
return RustColors.info_log;
|
||||
case "DEBUG":
|
||||
return RustColors.debug_log;
|
||||
case "WARN":
|
||||
return RustColors.warn_log;
|
||||
case "ERROR":
|
||||
return RustColors.error_log;
|
||||
case "TRACE":
|
||||
return RustColors.trace_log;
|
||||
default:
|
||||
return RustColors.info_log;
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
model: model
|
||||
|
||||
delegate: Text {
|
||||
color: listView.getLogColor(level)
|
||||
font.family: "monospace"
|
||||
text: `[${level}] ${message}`
|
||||
}
|
||||
|
||||
onCountChanged: Qt.callLater(positionViewAtEnd)
|
||||
}
|
||||
Connections {
|
||||
function onLogged(level, message) {
|
||||
model.append({
|
||||
level,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
target: Logger
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
Menu {
|
||||
background: Rectangle {
|
||||
border.color: RustColors.hovered
|
||||
border.width: 10
|
||||
color: RustColors.menubar
|
||||
implicitWidth: 100
|
||||
radius: 5
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
MenuBar {
|
||||
// Background for this MenuBar.
|
||||
background: Rectangle {
|
||||
color: RustColors.menubar
|
||||
}
|
||||
|
||||
//
|
||||
// File Menu
|
||||
ClideMenu {
|
||||
title: qsTr("&File")
|
||||
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionNewProject
|
||||
|
||||
text: qsTr("&New Project...")
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionOpen
|
||||
|
||||
text: qsTr("&Open...")
|
||||
}
|
||||
|
||||
onTriggered: FileSystem.setDirectory(FileSystem.filePath)
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionSave
|
||||
|
||||
text: qsTr("&Save")
|
||||
}
|
||||
}
|
||||
MenuSeparator {
|
||||
background: Rectangle {
|
||||
border.color: color
|
||||
color: Qt.darker(RustColors.menubar, 1)
|
||||
implicitHeight: 3
|
||||
implicitWidth: 200
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionExit
|
||||
|
||||
text: qsTr("&Exit")
|
||||
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Edit Menu
|
||||
ClideMenu {
|
||||
title: qsTr("&Edit")
|
||||
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionUndo
|
||||
|
||||
text: qsTr("&Undo")
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionRedo
|
||||
|
||||
text: qsTr("&Redo")
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionCut
|
||||
|
||||
text: qsTr("&Cut")
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionCopy
|
||||
|
||||
text: qsTr("&Copy")
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionPaste
|
||||
|
||||
text: qsTr("&Paste")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// View Menu
|
||||
ClideMenu {
|
||||
title: qsTr("&View")
|
||||
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionAppearance
|
||||
|
||||
text: qsTr("&Appearance")
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionToolWindows
|
||||
|
||||
text: qsTr("&Tool Windows")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Help Menu
|
||||
ClideAboutWindow {
|
||||
id: clideAbout
|
||||
|
||||
}
|
||||
ClideMenu {
|
||||
title: qsTr("&Help")
|
||||
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionDocumentation
|
||||
|
||||
text: qsTr("&Documentation")
|
||||
}
|
||||
}
|
||||
ClideMenuItem {
|
||||
action: Action {
|
||||
id: actionAbout
|
||||
|
||||
text: qsTr("&About")
|
||||
|
||||
// Toggle the about window with the menu item is clicked.
|
||||
onTriggered: clideAbout.visible = !clideAbout.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
MenuItem {
|
||||
id: root
|
||||
|
||||
background: Rectangle {
|
||||
color: root.hovered ? RustColors.hovered : RustColors.unhovered
|
||||
radius: 1.0
|
||||
}
|
||||
contentItem: IconLabel {
|
||||
color: "black"
|
||||
font.family: "Helvetica"
|
||||
text: root.text
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import clide.module 1.0
|
||||
|
||||
ScrollBar {
|
||||
id: scrollBar
|
||||
|
||||
// Height, opacitiy, width
|
||||
property int h: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
|
||||
property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
|
||||
property int sizeModifier: 4
|
||||
property int w: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
|
||||
|
||||
// Scroll bar gutter
|
||||
background: Rectangle {
|
||||
id: gutter
|
||||
|
||||
color: RustColors.scrollbar_gutter
|
||||
implicitHeight: scrollBar.h
|
||||
implicitWidth: scrollBar.w
|
||||
|
||||
// Fade the scrollbar gutter when inactive.
|
||||
opacity: scrollBar.o
|
||||
radius: 20
|
||||
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll bar
|
||||
contentItem: Rectangle {
|
||||
readonly property color barColor: {
|
||||
if (scrollBar.size < 1.0) {
|
||||
// If the scrollbar is required change it's color based on activity.
|
||||
if (scrollBar.active) {
|
||||
return RustColors.scrollbar_active;
|
||||
} else {
|
||||
return RustColors.scrollbar;
|
||||
}
|
||||
} else {
|
||||
// If we don't need a scrollbar, fallback to the gutter color.
|
||||
return gutter.color;
|
||||
}
|
||||
}
|
||||
|
||||
color: barColor
|
||||
implicitHeight: scrollBar.h
|
||||
implicitWidth: scrollBar.w
|
||||
|
||||
// Fade the scrollbar when inactive.
|
||||
opacity: scrollBar.o
|
||||
radius: 20
|
||||
|
||||
// Smooth transition between color changes based on the state above.
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 1000
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
ClideScrollBar ClideScrollBar.qml
|
||||
ClideHandle ClideHandle.qml
|
||||
ClideMenu ClideMenu.qml
|
||||
ClideMenuItem ClideMenuItem.qml
|
||||
ClideBreadCrumbs ClideBreadCrumbs.qml
|
||||
ClideAboutWindow ClideAboutWindow.qml
|
||||
ClideEditor ClideEditor.qml
|
||||
ClideLogger ClideLogger.qml
|
||||
ClideMenuBar ClideMenuBar.qml
|
||||
@@ -1,35 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
signal logged(string level, string message)
|
||||
|
||||
function debug(msg) {
|
||||
console.log(msg);
|
||||
logged("DEBUG", msg);
|
||||
}
|
||||
function error(msg) {
|
||||
console.error(msg);
|
||||
logged("ERROR", msg);
|
||||
}
|
||||
function info(msg) {
|
||||
console.log(msg);
|
||||
logged("INFO", msg);
|
||||
}
|
||||
function log(msg) {
|
||||
console.log(msg);
|
||||
logged("INFO", msg);
|
||||
}
|
||||
function trace(msg) {
|
||||
console.log(msg);
|
||||
logged("TRACE", msg);
|
||||
}
|
||||
function warn(msg) {
|
||||
console.warn(msg);
|
||||
logged("WARN", msg);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
singleton Logger 1.0 Logger.qml
|
||||
28
qml/main.qml
28
qml/main.qml
@@ -12,32 +12,28 @@ import clide.module 1.0
|
||||
ApplicationWindow {
|
||||
id: appWindow
|
||||
|
||||
required property string appContextPath
|
||||
|
||||
height: 800
|
||||
title: "Clide"
|
||||
title: "CLIDE"
|
||||
visible: true
|
||||
width: 1200
|
||||
|
||||
required property string appContextPath
|
||||
|
||||
menuBar: ClideMenuBar {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: RustColors.menubar
|
||||
width: appView.implicitWidth
|
||||
height: appView.implicitHeight
|
||||
anchors.fill: parent
|
||||
color: RustColors.gutter
|
||||
}
|
||||
|
||||
ClideApplicationView {
|
||||
id: appView
|
||||
projectDir: appWindow.appContextPath
|
||||
implicitHeight: appWindow.height
|
||||
implicitWidth: appWindow.width
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 20
|
||||
anchors.topMargin: 10
|
||||
}
|
||||
title: qsTr("Error")
|
||||
}
|
||||
ClideProjectView {
|
||||
projectDir: appWindow.appContextPath
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/images">
|
||||
<file alias="kilroy.png">resources/images/kilroy-256.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="/fonts">
|
||||
<file alias="saucecodepro.ttf">resources/SauceCodeProNerdFont-Black.ttf</file>
|
||||
<file alias="saucecodepro-light.ttf">resources/SauceCodeProNerdFont-Light.ttf</file>
|
||||
<file alias="saucecodepro-xlight.ttf">resources/SauceCodeProNerdFont-ExtraLight.ttf</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 402 KiB |
@@ -36,12 +36,6 @@ pub mod qobject {
|
||||
#[qproperty(QColor, explorer_background)]
|
||||
#[qproperty(QColor, explorer_folder)]
|
||||
#[qproperty(QColor, explorer_folder_open)]
|
||||
#[qproperty(QColor, terminal_background)]
|
||||
#[qproperty(QColor, info_log)]
|
||||
#[qproperty(QColor, debug_log)]
|
||||
#[qproperty(QColor, warn_log)]
|
||||
#[qproperty(QColor, error_log)]
|
||||
#[qproperty(QColor, trace_log)]
|
||||
type RustColors = super::RustColorsImpl;
|
||||
}
|
||||
}
|
||||
@@ -71,12 +65,6 @@ pub struct RustColorsImpl {
|
||||
explorer_background: QColor,
|
||||
explorer_folder: QColor,
|
||||
explorer_folder_open: QColor,
|
||||
terminal_background: QColor,
|
||||
info_log: QColor,
|
||||
debug_log: QColor,
|
||||
warn_log: QColor,
|
||||
error_log: QColor,
|
||||
trace_log: QColor,
|
||||
}
|
||||
|
||||
impl Default for RustColorsImpl {
|
||||
@@ -85,7 +73,7 @@ impl Default for RustColorsImpl {
|
||||
hovered: QColor::try_from("#303234").unwrap(),
|
||||
unhovered: QColor::try_from("#3c3f41").unwrap(),
|
||||
pressed: QColor::try_from("#4b4f51").unwrap(),
|
||||
menubar: QColor::try_from("#262626").unwrap(),
|
||||
menubar: QColor::try_from("#3c3f41").unwrap(),
|
||||
menubar_border: QColor::try_from("#575757").unwrap(),
|
||||
scrollbar: QColor::try_from("#4b4f51").unwrap(),
|
||||
scrollbar_active: QColor::try_from("#4b4f51").unwrap(),
|
||||
@@ -93,23 +81,17 @@ impl Default for RustColorsImpl {
|
||||
linenumber: QColor::try_from("#94989b").unwrap(),
|
||||
active: QColor::try_from("#a9acb0").unwrap(),
|
||||
inactive: QColor::try_from("#FFF").unwrap(),
|
||||
editor_background: QColor::try_from("#1E1F22").unwrap(),
|
||||
editor_background: QColor::try_from("#2b2b2b").unwrap(),
|
||||
editor_text: QColor::try_from("#acaea3").unwrap(),
|
||||
editor_highlighted_text: QColor::try_from("#ccced3").unwrap(),
|
||||
editor_highlight: QColor::try_from("#ccced3").unwrap(),
|
||||
gutter: QColor::try_from("#1e1f22").unwrap(),
|
||||
explorer_hovered: QColor::try_from("#4c5053").unwrap(),
|
||||
explorer_text: QColor::try_from("#FFF").unwrap(),
|
||||
explorer_text_selected: QColor::try_from("#262626").unwrap(),
|
||||
explorer_background: QColor::try_from("#1E1F22").unwrap(),
|
||||
explorer_text: QColor::try_from("#3b3b3b").unwrap(),
|
||||
explorer_text_selected: QColor::try_from("#8b8b8b").unwrap(),
|
||||
explorer_background: QColor::try_from("#676c70").unwrap(),
|
||||
explorer_folder: QColor::try_from("#54585b").unwrap(),
|
||||
explorer_folder_open: QColor::try_from("#393B40").unwrap(),
|
||||
terminal_background: QColor::try_from("#111111").unwrap(),
|
||||
info_log: QColor::try_from("#C4FFFF").unwrap(),
|
||||
debug_log: QColor::try_from("#9148AF").unwrap(),
|
||||
warn_log: QColor::try_from("#C4A958").unwrap(),
|
||||
error_log: QColor::try_from("#ff5555").unwrap(),
|
||||
trace_log: QColor::try_from("#ffaa00").unwrap(),
|
||||
explorer_folder_open: QColor::try_from("#FFF").unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,86 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
use crate::gui::filesystem::qobject::{QAbstractItemModel};
|
||||
use cxx_qt_lib::{QModelIndex, QString};
|
||||
use devicons::FileIcon;
|
||||
use dirs;
|
||||
use log::warn;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use syntect::easy::HighlightLines;
|
||||
use std::io::BufRead;
|
||||
use std::{fs};
|
||||
use std::pin::Pin;
|
||||
use syntect::easy::HighlightFile;
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use syntect::html::{IncludeBackground, append_highlighted_html_for_styled_line};
|
||||
use syntect::html::{
|
||||
IncludeBackground, append_highlighted_html_for_styled_line, start_highlighted_html_snippet,
|
||||
};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
#[cxx_qt::bridge]
|
||||
pub mod qobject {
|
||||
// Import Qt Types from C++
|
||||
unsafe extern "C++" {
|
||||
// Import Qt Types from C++
|
||||
include!("cxx-qt-lib/qstring.h");
|
||||
type QString = cxx_qt_lib::QString;
|
||||
|
||||
include!("cxx-qt-lib/qmodelindex.h");
|
||||
type QModelIndex = cxx_qt_lib::QModelIndex;
|
||||
|
||||
include!(<QtGui/QFileSystemModel>);
|
||||
type QFileSystemModel;
|
||||
|
||||
include!(<QSortFilterProxyModel>);
|
||||
type QSortFilterProxyModel;
|
||||
|
||||
include!(<QAbstractItemModel>);
|
||||
type QAbstractItemModel;
|
||||
}
|
||||
|
||||
// Export QML classes from Rust
|
||||
unsafe extern "RustQt" {
|
||||
// Export QML Types from Rust
|
||||
#[qobject]
|
||||
#[qml_element]
|
||||
#[base = QSortFilterProxyModel]
|
||||
#[qproperty(*mut FileSystem, inner)]
|
||||
type FileSystemSortProxyModel = super::FileSystemSortProxyModelImpl;
|
||||
|
||||
#[qobject]
|
||||
#[base = QFileSystemModel]
|
||||
#[qml_element]
|
||||
#[qml_singleton]
|
||||
#[qproperty(QString, file_path, cxx_name = "filePath")]
|
||||
#[qproperty(QModelIndex, root_index, cxx_name = "rootIndex")]
|
||||
type FileSystem = super::FileSystemImpl;
|
||||
}
|
||||
|
||||
// Export QSortFilterProxyModel functions from Rust
|
||||
// https://doc.qt.io/qt-6/qsortfilterproxymodel.html
|
||||
unsafe extern "RustQt" {
|
||||
#[inherit]
|
||||
#[cxx_name = "setSourceModel"]
|
||||
unsafe fn set_source_model(
|
||||
self: Pin<&mut FileSystemSortProxyModel>,
|
||||
source: *mut QAbstractItemModel,
|
||||
);
|
||||
|
||||
#[cxx_override]
|
||||
#[cxx_name = "filterAcceptsRow"]
|
||||
const fn filter_accepts_row(
|
||||
self: &FileSystemSortProxyModel,
|
||||
source_row: i32,
|
||||
source_parent: &QModelIndex,
|
||||
) -> bool;
|
||||
|
||||
#[qinvokable]
|
||||
#[cxx_name = "setDirectory"]
|
||||
fn set_directory(self: Pin<&mut FileSystemSortProxyModel>, path: &QString) -> QModelIndex;
|
||||
}
|
||||
|
||||
// Custom initialization logic.
|
||||
impl cxx_qt::Initialize for FileSystemSortProxyModel {}
|
||||
|
||||
// Export QFileSystemModel functions from Rust
|
||||
// https://doc.qt.io/qt-6/qfilesystemmodel.html
|
||||
unsafe extern "RustQt" {
|
||||
#[inherit]
|
||||
#[cxx_name = "setRootPath"]
|
||||
fn set_root_path(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex;
|
||||
@@ -47,18 +93,72 @@ pub mod qobject {
|
||||
#[qinvokable]
|
||||
#[cxx_name = "readFile"]
|
||||
fn read_file(self: &FileSystem, path: &QString) -> QString;
|
||||
|
||||
#[qinvokable]
|
||||
#[cxx_name = "setDirectory"]
|
||||
fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex;
|
||||
|
||||
#[qinvokable]
|
||||
fn icon(self: Pin<&mut FileSystem>, path: &QString) -> QString;
|
||||
//
|
||||
// #[qinvokable]
|
||||
// #[cxx_name = "setDirectory"]
|
||||
// fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileSystemSortProxyModelImpl {
|
||||
inner: *mut qobject::FileSystem,
|
||||
}
|
||||
|
||||
impl Default for FileSystemSortProxyModelImpl {
|
||||
fn default() -> Self {
|
||||
let model = Self {
|
||||
inner: std::ptr::null_mut(),
|
||||
};
|
||||
model
|
||||
}
|
||||
}
|
||||
|
||||
impl qobject::FileSystemSortProxyModel {
|
||||
pub const fn filter_accepts_row(&self, _source_row: i32, _source_parent: &QModelIndex) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn read_file(&self, path: &QString) -> QString {
|
||||
if let Some(inner) = unsafe { self.inner().as_ref() } {
|
||||
let pinned_inner = unsafe { Pin::new_unchecked(inner) };
|
||||
return pinned_inner.read_file(path)
|
||||
} else {
|
||||
panic!("Can't get inner()")
|
||||
}
|
||||
QString::default()
|
||||
}
|
||||
|
||||
// There will never be more than one column.
|
||||
fn column_count(&self, _index: &QModelIndex) -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn set_directory(self: Pin<&mut Self>, path: &QString) -> QModelIndex {
|
||||
if let Some(inner) = unsafe { self.inner().as_mut() } {
|
||||
let pinned_inner = unsafe { Pin::new_unchecked(inner) };
|
||||
return pinned_inner.set_directory(path)
|
||||
} else {
|
||||
panic!("Can't get inner()")
|
||||
}
|
||||
QModelIndex::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl cxx_qt::Initialize for qobject::FileSystemSortProxyModel {
|
||||
fn initialize(self: core::pin::Pin<&mut Self>) {
|
||||
let mut fs = FileSystemImpl::default();
|
||||
unsafe {
|
||||
let model: *mut FileSystemImpl = std::ptr::from_mut(&mut fs);
|
||||
let m: *mut QAbstractItemModel = model as *mut QAbstractItemModel;
|
||||
self.set_source_model(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Impleent a provider for QFileSystemModel::setIconProvider for icons.
|
||||
pub struct FileSystemImpl {
|
||||
file_path: QString,
|
||||
root_index: QModelIndex,
|
||||
}
|
||||
|
||||
// Default is explicit to make the editor open this source file initially.
|
||||
@@ -66,6 +166,7 @@ impl Default for FileSystemImpl {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_path: QString::from(file!()),
|
||||
root_index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,47 +176,42 @@ impl qobject::FileSystem {
|
||||
if path.is_empty() {
|
||||
return QString::default();
|
||||
}
|
||||
let meta = fs::metadata(path.to_string())
|
||||
.expect(format!("Failed to get file metadata {path:?}").as_str());
|
||||
if !meta.is_file() {
|
||||
if !fs::metadata(path.to_string())
|
||||
.expect(format!("Failed to get file metadata {path:?}").as_str())
|
||||
.is_file()
|
||||
{
|
||||
warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file");
|
||||
return QString::default();
|
||||
}
|
||||
let path_str = path.to_string();
|
||||
if let Ok(lines) = fs::read_to_string(path_str.as_str()) {
|
||||
let ss = SyntaxSet::load_defaults_nonewlines();
|
||||
let ts = ThemeSet::load_defaults();
|
||||
let theme = &ts.themes["base16-ocean.dark"];
|
||||
let lang = ss
|
||||
.find_syntax_by_extension(
|
||||
Path::new(path_str.as_str())
|
||||
.extension()
|
||||
.map(|s| s.to_str())
|
||||
.unwrap_or_else(|| Some("md"))
|
||||
.expect("Failed to get file extension"),
|
||||
)
|
||||
.unwrap_or_else(|| ss.find_syntax_plain_text());
|
||||
let mut highlighter = HighlightLines::new(lang, theme);
|
||||
// If you care about the background, see `start_highlighted_html_snippet(theme);`.
|
||||
let mut output = String::from("<pre>\n");
|
||||
for line in LinesWithEndings::from(lines.as_str()) {
|
||||
let regions = highlighter
|
||||
.highlight_line(line, &ss)
|
||||
.expect("Failed to highlight");
|
||||
let ss = SyntaxSet::load_defaults_nonewlines();
|
||||
let ts = ThemeSet::load_defaults();
|
||||
let theme = &ts.themes["base16-ocean.dark"];
|
||||
|
||||
append_highlighted_html_for_styled_line(
|
||||
®ions[..],
|
||||
IncludeBackground::No,
|
||||
&mut output,
|
||||
)
|
||||
.expect("Failed to insert highlighted html");
|
||||
}
|
||||
let mut highlighter =
|
||||
HighlightFile::new(path.to_string(), &ss, theme).expect("Failed to create highlighter");
|
||||
let (mut output, _bg) = start_highlighted_html_snippet(theme);
|
||||
let mut line = String::new();
|
||||
while highlighter
|
||||
.reader
|
||||
.read_line(&mut line)
|
||||
.expect("Failed to read file.")
|
||||
> 0
|
||||
{
|
||||
let regions = highlighter
|
||||
.highlight_lines
|
||||
.highlight_line(&line, &ss)
|
||||
.expect("Failed to highlight");
|
||||
|
||||
output.push_str("</pre>\n");
|
||||
QString::from(output)
|
||||
} else {
|
||||
return QString::default();
|
||||
append_highlighted_html_for_styled_line(
|
||||
®ions[..],
|
||||
IncludeBackground::Yes,
|
||||
&mut output,
|
||||
)
|
||||
.expect("Failed to insert highlighted html");
|
||||
line.clear();
|
||||
}
|
||||
output.push_str("</pre>\n");
|
||||
QString::from(output)
|
||||
}
|
||||
|
||||
// There will never be more than one column.
|
||||
@@ -132,24 +228,14 @@ impl qobject::FileSystem {
|
||||
self.set_root_path(path)
|
||||
} else {
|
||||
// If the initial directory can't be opened, attempt to find the home directory.
|
||||
let homedir = dirs::home_dir()
|
||||
.expect("Failed to get home directory")
|
||||
.as_path()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
self.set_root_path(&QString::from(homedir))
|
||||
self.set_root_path(&QString::from(
|
||||
dirs::home_dir()
|
||||
.expect("Failed to get home directory")
|
||||
.as_path()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn icon(self: std::pin::Pin<&mut Self>, path: &QString) -> QString {
|
||||
let str = path.to_string();
|
||||
if Path::new(&str).is_dir() {
|
||||
// Ensures directories are given a folder icon and not mistakenly resolved to a language.
|
||||
// For example, a directory named `cpp` would otherwise return a C++ icon.
|
||||
return QString::from(FileIcon::from("dir/").to_string())
|
||||
}
|
||||
let icon = FileIcon::from(str);
|
||||
QString::from(icon.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user