9 Commits

Author SHA1 Message Date
67bf82d0cb Add qmllint.ini 2026-02-06 18:59:38 -05:00
39377b32f0 WIP 2026-02-06 18:59:02 -05:00
176efb97b7 Clean up ClideScrollBar. 2026-02-04 19:52:05 -05:00
755066d847 Clean up ClideHandle. 2026-02-04 19:29:52 -05:00
773d7818b5 Auto scroll logger. 2026-02-04 18:35:43 -05:00
7e58e3ee03 Split basic components for reuse. 2026-02-02 23:05:53 -05:00
0f50577d78 Fix scrollbar animations. 2026-02-02 18:21:28 -05:00
29024e3999 Format with qmlformat. 2026-02-02 18:08:37 -05:00
5af09485a3 Add trace logs. 2026-02-02 18:01:53 -05:00
14 changed files with 438 additions and 327 deletions

33
.qmllint.ini Normal file
View File

@@ -0,0 +1,33 @@
[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

View File

@@ -9,6 +9,8 @@ fn main() {
"qml/ClideEditor.qml", "qml/ClideEditor.qml",
"qml/ClideMenuBar.qml", "qml/ClideMenuBar.qml",
"qml/ClideLogger.qml", "qml/ClideLogger.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Components/ClideHandle.qml",
"qml/Logger/Logger.qml", "qml/Logger/Logger.qml",
])) ]))
// Link Qt's Network library // Link Qt's Network library

View File

@@ -6,18 +6,22 @@ import QtQuick
import QtQuick.Controls.Basic import QtQuick.Controls.Basic
import clide.module 1.0 import clide.module 1.0
import Logger 1.0
ApplicationWindow { ApplicationWindow {
id: root id: root
width: 450
height: 350
// Create the window with no frame and keep it on top.
flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
color: RustColors.gutter 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
// Hide the window when it loses focus. // Hide the window when it loses focus.
onActiveChanged: { onActiveChanged: {
if (!active) { Logger.debug("Setting active: " + root.active)
if (!root.active) {
root.visible = false; root.visible = false;
} }
} }
@@ -27,48 +31,38 @@ ApplicationWindow {
id: logo id: logo
anchors.left: parent.left anchors.left: parent.left
anchors.margins: 20
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.margins: 20
source: "qrc:/images/kilroy.png"
sourceSize.width: 80
sourceSize.height: 80
fillMode: Image.PreserveAspectFit
smooth: true
antialiasing: true antialiasing: true
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/images/kilroy.png"
sourceSize.height: 80
sourceSize.width: 80
} }
ScrollView { ScrollView {
anchors.top: logo.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: 20 anchors.margins: 20
anchors.right: parent.right
anchors.top: logo.bottom
TextArea { TextArea {
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
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 antialiasing: true
background: null 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>")
textFormat: Text.RichText
wrapMode: Text.WordWrap
onLinkActivated: function (link) { onLinkActivated: function (link) {
Qt.openUrlExternally(link) Qt.openUrlExternally(link);
} }
} }
} }

View File

@@ -11,35 +11,38 @@ import Logger 1.0
SplitView { SplitView {
id: root id: root
Layout.fillHeight: true
Layout.fillWidth: true
orientation: Qt.Vertical
// The path to the file to show in the text editor. // The path to the file to show in the text editor.
// This is updated by a signal caught within ClideProjectView. // This is updated by a signal caught within ClideProjectView.
// Initialized by the Default trait for the Rust QML singleton FileSystem. // Initialized by the Default trait for the Rust QML singleton FileSystem.
required property string filePath required property string filePath
// Customized handle to drag between the Editor and the Console. Layout.fillHeight: true
handle: Rectangle { Layout.fillWidth: true
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter orientation: Qt.Vertical
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
implicitHeight: 8
radius: 2.5
// Execute these behaviors when the color is changed. // Customized handle to drag between the Editor and the Console.
Behavior on color { handle: ClideHandle {
ColorAnimation { pressed: SplitHandle.pressed
duration: 400 hovered: SplitHandle.hovered
}
}
} }
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");
}
RowLayout { RowLayout {
// We use a flickable to synchronize the position of the editor and // We use a flickable to synchronize the position of the editor and
// the line numbers. This is necessary because the line numbers can // the line numbers. This is necessary because the line numbers can
// extend the available height. // extend the available height.
Flickable { Flickable {
id: lineNumbers id: lineNumbers
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: false Layout.fillWidth: false
// Calculating the width correctly is important as the number grows. // Calculating the width correctly is important as the number grows.
@@ -58,20 +61,22 @@ SplitView {
Repeater { Repeater {
id: repeatedLineNumbers id: repeatedLineNumbers
// TODO: Bug where text wrapping shows as new line number. // TODO: Bug where text wrapping shows as new line number.
model: textArea.lineCount model: textArea.lineCount
// This Item is used for each line number in the gutter. // This Item is used for each line number in the gutter.
delegate: Item { delegate: Item {
required property int index
// Calculates the height of each line in the text area. // Calculates the height of each line in the text area.
height: textArea.contentHeight / textArea.lineCount height: textArea.contentHeight / textArea.lineCount
width: parent.width width: parent.width
required property int index
// Show the line number. // Show the line number.
Label { Label {
id: numbers id: numbers
color: RustColors.linenumber color: RustColors.linenumber
font: textArea.font font: textArea.font
height: parent.height height: parent.height
@@ -79,6 +84,7 @@ SplitView {
text: parent.index + 1 text: parent.index + 1
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width width: parent.width - indicator.width
background: Rectangle { background: Rectangle {
color: RustColors.terminal_background color: RustColors.terminal_background
} }
@@ -86,6 +92,7 @@ SplitView {
// Draw edge along the right side of the line number. // Draw edge along the right side of the line number.
Rectangle { Rectangle {
id: indicator id: indicator
anchors.left: numbers.right anchors.left: numbers.right
color: RustColors.linenumber color: RustColors.linenumber
height: parent.height height: parent.height
@@ -97,27 +104,28 @@ SplitView {
} }
Flickable { Flickable {
id: editorFlickable id: editorFlickable
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
height: 650 height: 650
ScrollBar.horizontal: MyScrollBar { ScrollBar.horizontal: ClideScrollBar {
} }
ScrollBar.vertical: MyScrollBar { ScrollBar.vertical: ClideScrollBar {
} }
TextArea.flickable: TextArea { TextArea.flickable: TextArea {
id: textArea id: textArea
antialiasing: true
focus: true focus: true
persistentSelection: true persistentSelection: true
antialiasing: true
selectByMouse: true selectByMouse: true
selectionColor: RustColors.editor_highlight
selectedTextColor: RustColors.editor_highlighted_text selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
text: FileSystem.readFile(root.filePath)
textFormat: Qt.AutoText textFormat: Qt.AutoText
wrapMode: TextArea.Wrap wrapMode: TextArea.Wrap
text: FileSystem.readFile(root.filePath)
onLinkActivated: function (link) { onLinkActivated: function (link) {
Qt.openUrlExternally(link); Qt.openUrlExternally(link);
@@ -155,56 +163,6 @@ SplitView {
} }
ClideLogger { ClideLogger {
id: areaConsole id: areaConsole
}
// 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
}
}
}
}
Component.onCompleted: {
// Show logging is working.
Logger.debug("Debug console ready")
Logger.warn("Warnings show up too")
} }
} }

View File

@@ -9,50 +9,54 @@ import clide.module 1.0
import Logger 1.0 import Logger 1.0
Item { Item {
ListModel { id: model } ListModel {
id: model
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "#111" color: "#111"
} }
ListView { ListView {
id: listView id: listView
anchors.fill: parent
model: model
clip: true
function getLogColor(level) { function getLogColor(level) {
switch (level) { switch (level) {
case "INFO": case "INFO":
return RustColors.info_log return RustColors.info_log;
break; case "DEBUG":
case "DEBUG": return RustColors.debug_log;
return RustColors.debug_log case "WARN":
break; return RustColors.warn_log;
case "WARN": case "ERROR":
return RustColors.warn_log return RustColors.error_log;
break; case "TRACE":
case "ERROR": return RustColors.trace_log;
return RustColors.error_log default:
break; return RustColors.info_log;
default:
return RustColors.info_log
break;
} }
} }
delegate: Text { anchors.fill: parent
text: `[${level}] ${message}` clip: true
font.family: "monospace" model: model
color: listView.getLogColor(level)
}
}
Connections { delegate: Text {
target: Logger color: listView.getLogColor(level)
function onLogged(level, message) { font.family: "monospace"
model.append({ level, message }) text: `[${level}] ${message}`
} }
onCountChanged: Qt.callLater(positionViewAtEnd)
}
Connections {
function onLogged(level, message) {
model.append({
level,
message
});
}
target: Logger
} }
} }

View File

@@ -10,32 +10,8 @@ import clide.module 1.0
MenuBar { MenuBar {
// Background for this MenuBar. // Background for this MenuBar.
background: Rectangle { background: Rectangle {
color: RustColors.menubar
border.color: RustColors.explorer_background border.color: RustColors.explorer_background
} color: RustColors.menubar
// Base settings for each Menu.
component ClideMenu : Menu {
background: Rectangle {
color: RustColors.explorer_background
implicitWidth: 100
radius: 2
}
}
// Base settings for each MenuItem.
component ClideMenuItem : 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
}
} }
// //
@@ -70,6 +46,7 @@ MenuBar {
} }
ClideMenuItem { ClideMenuItem {
action: actionOpen action: actionOpen
onTriggered: FileSystem.setDirectory(FileSystem.filePath) onTriggered: FileSystem.setDirectory(FileSystem.filePath)
} }
ClideMenuItem { ClideMenuItem {
@@ -162,8 +139,8 @@ MenuBar {
// Help Menu // Help Menu
ClideAboutWindow { ClideAboutWindow {
id: clideAbout id: clideAbout
}
}
Action { Action {
id: actionDocumentation id: actionDocumentation
@@ -171,10 +148,11 @@ MenuBar {
} }
Action { Action {
id: actionAbout id: actionAbout
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
text: qsTr("&About") text: qsTr("&About")
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
} }
ClideMenu { ClideMenu {
title: qsTr("&Help") title: qsTr("&Help")
@@ -186,4 +164,28 @@ MenuBar {
action: actionAbout action: actionAbout
} }
} }
// Base settings for each Menu.
component ClideMenu: Menu {
background: Rectangle {
color: RustColors.explorer_background
implicitWidth: 100
radius: 2
}
}
// Base settings for each MenuItem.
component ClideMenuItem: 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
}
}
} }

View File

@@ -22,6 +22,7 @@ SplitView {
// Customized handle to drag between the Navigation and the Editor. // Customized handle to drag between the Navigation and the Editor.
handle: Rectangle { handle: Rectangle {
id: verticalSplitHandle id: verticalSplitHandle
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
implicitWidth: 8 implicitWidth: 8
@@ -37,65 +38,71 @@ SplitView {
Rectangle { Rectangle {
id: navigationView id: navigationView
color: RustColors.explorer_background
SplitView.fillHeight: true SplitView.fillHeight: true
SplitView.maximumWidth: 250
SplitView.minimumWidth: 0 SplitView.minimumWidth: 0
SplitView.preferredWidth: 200 SplitView.preferredWidth: 200
SplitView.maximumWidth: 250 color: RustColors.explorer_background
ColumnLayout { ColumnLayout {
spacing: 2 spacing: 2
// TODO: Make a ClideBreadCrumb element to support select parent paths as root // TODO: Make a ClideBreadCrumb element to support select parent paths as root
Rectangle { Rectangle {
width: navigationView.width
height: 25
color: RustColors.explorer_background color: RustColors.explorer_background
height: 25
width: navigationView.width
Text { Text {
id: breadCrumb id: breadCrumb
anchors.fill: parent anchors.fill: parent
text: clideTreeView.rootDirectory
color: RustColors.explorer_text color: RustColors.explorer_text
elide: Text.ElideLeft elide: Text.ElideLeft
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: clideTreeView.rootDirectory
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onSingleTapped: (eventPoint, button) => contextMenu.popup() onSingleTapped: (eventPoint, button) => contextMenu.popup()
} }
Menu { Menu {
id: contextMenu id: contextMenu
Action { Action {
text: qsTr("Reset root index") text: qsTr("Reset root index")
onTriggered: { onTriggered: {
Logger.log("Resetting root directory: " + clideTreeView.originalRootDirectory) Logger.log("Resetting root directory: " + clideTreeView.originalRootDirectory);
clideTreeView.rootDirectory = clideTreeView.originalRootDirectory clideTreeView.rootDirectory = clideTreeView.originalRootDirectory;
} }
} }
} }
} }
ClideTreeView { ClideTreeView {
id: clideTreeView id: clideTreeView
onFileClicked: path => clideEditor.filePath = path
width: navigationView.width
height: navigationView.height height: navigationView.height
// Path to the directory opened in the file explorer. // Path to the directory opened in the file explorer.
originalRootDirectory: root.projectDir originalRootDirectory: root.projectDir
rootDirectory: root.projectDir rootDirectory: root.projectDir
width: navigationView.width
onFileClicked: path => clideEditor.filePath = path
onRootDirectoryChanged: { onRootDirectoryChanged: {
Logger.log(clideTreeView.rootDirectory) Logger.log("Setting root directory: " + clideTreeView.rootDirectory);
breadCrumb.text = clideTreeView.rootDirectory breadCrumb.text = clideTreeView.rootDirectory;
} }
} }
} }
} }
ClideEditor { ClideEditor {
id: clideEditor id: clideEditor
SplitView.fillWidth: true SplitView.fillWidth: true
// Provide a path to the file currently open in the text editor. // Provide a path to the file currently open in the text editor.

View File

@@ -11,142 +11,19 @@ import Logger 1.0
TreeView { TreeView {
id: fileSystemTreeView id: fileSystemTreeView
model: FileSystem
property int lastIndex: -1 property int lastIndex: -1
required property string originalRootDirectory required property string originalRootDirectory
property string rootDirectory property string rootDirectory
signal fileClicked(string filePath) signal fileClicked(string filePath)
rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory)
leftMargin: 5
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds
clip: true clip: true
leftMargin: 5
// The delegate represents a single entry in the filesystem. model: FileSystem
delegate: TreeViewDelegate { rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory)
id: treeDelegate
indentation: 12
implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
implicitHeight: 25
required property int index
required property url filePath
required property string fileName
indicator: Image {
id: directoryIcon
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) {
return treeDelegate.expanded ?
folderOpen : folderClosed;
} else {
return file
}
}
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
}
contentItem: Text {
text: treeDelegate.fileName
color: RustColors.explorer_text
}
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
}
}
}
MultiEffect {
id: iconOverlay
anchors.fill: directoryIcon
source: directoryIcon
colorization: 1.0
brightness: 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
}
}
HoverHandler {
id: hoverHandler
acceptedDevices: PointerDevice.Mouse
}
TapHandler {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onSingleTapped: (eventPoint, button) => {
switch (button) {
case Qt.LeftButton:
fileSystemTreeView.toggleExpanded(treeDelegate.row)
// If this model item doesn't have children, it means it's
// representing a file.
if (!treeDelegate.hasChildren)
fileSystemTreeView.fileClicked(treeDelegate.filePath)
break;
case Qt.RightButton:
contextMenu.popup();
break;
}
}
}
Menu {
id: contextMenu
Action {
text: qsTr("Set as root index")
enabled: treeDelegate.hasChildren
onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath)
fileSystemTreeView.rootDirectory = treeDelegate.filePath
}
}
Action {
text: qsTr("Reset root index")
onTriggered: {
Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory)
fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory
}
}
}
}
// Provide our own custom ScrollIndicator for the TreeView. // Provide our own custom ScrollIndicator for the TreeView.
ScrollIndicator.vertical: ScrollIndicator { ScrollIndicator.vertical: ScrollIndicator {
@@ -154,10 +31,9 @@ TreeView {
implicitWidth: 15 implicitWidth: 15
contentItem: Rectangle { contentItem: Rectangle {
implicitWidth: 6
implicitHeight: 6
color: RustColors.scrollbar color: RustColors.scrollbar
implicitHeight: 6
implicitWidth: 6
opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0
Behavior on opacity { Behavior on opacity {
@@ -167,4 +43,122 @@ TreeView {
} }
} }
} }
// 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: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
indentation: 12
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")
opacity: hoverHandler.hovered ? 0.75 : 1.0
radius: 2.5
Behavior on color {
ColorAnimation {
duration: 300
}
}
}
contentItem: Text {
color: RustColors.explorer_text
text: treeDelegate.fileName
}
indicator: Image {
id: directoryIcon
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) {
return treeDelegate.expanded ? folderOpen : folderClosed;
}
return file;
}
anchors.verticalCenter: parent.verticalCenter
antialiasing: true
asynchronous: true
fillMode: Image.PreserveAspectFit
smooth: true
source: setSourceImage()
sourceSize.height: 15
sourceSize.width: 15
x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation)
}
MultiEffect {
id: iconOverlay
anchors.fill: directoryIcon
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: directoryIcon
}
HoverHandler {
id: hoverHandler
acceptedDevices: PointerDevice.Mouse
}
TapHandler {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onSingleTapped: (eventPoint, button) => {
switch (button) {
case Qt.LeftButton:
fileSystemTreeView.toggleExpanded(treeDelegate.row);
// If this model item doesn't have children, it means it's
// representing a file.
if (!treeDelegate.hasChildren)
fileSystemTreeView.fileClicked(treeDelegate.filePath);
break;
case Qt.RightButton:
contextMenu.popup();
break;
}
}
}
Menu {
id: contextMenu
Action {
enabled: treeDelegate.hasChildren
text: qsTr("Set as root index")
onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath);
fileSystemTreeView.rootDirectory = treeDelegate.filePath;
}
}
Action {
text: qsTr("Reset root index")
onTriggered: {
Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory);
fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory;
}
}
}
}
} }

View File

@@ -0,0 +1,34 @@
// 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 {
readonly property color currentColor: {
if (pressed) {
return RustColors.pressed;
} else if (hovered) {
return RustColors.hovered;
} else {
return RustColors.gutter;
}
}
required property bool hovered
required property bool pressed
border.color: currentColor
color: currentColor
implicitHeight: 8
radius: 2.5
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 400
}
}
}

View File

@@ -0,0 +1,71 @@
// 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 ? 4 * 2 : 4
property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
property int w: scrollBar.interactive ? 4 * 2 : 4
// 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
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
// Smooth transition between color changes based on the state above.
Behavior on color {
ColorAnimation {
duration: 1000
}
}
Behavior on opacity {
OpacityAnimator {
duration: 1000
}
}
}
}

2
qml/Components/qmldir Normal file
View File

@@ -0,0 +1,2 @@
ClideScrollBar ClideScrollBar.qml
ClideHandle ClideHandle.qml

View File

@@ -8,23 +8,28 @@ import QtQuick
QtObject { QtObject {
signal logged(string level, string message) signal logged(string level, string message)
function log(msg) {
console.log(msg)
logged("INFO", msg)
}
function debug(msg) { function debug(msg) {
console.log(msg) console.log(msg);
logged("DEBUG", msg) logged("DEBUG", msg);
} }
function warn(msg) {
console.warn(msg)
logged("WARN", msg)
}
function error(msg) { function error(msg) {
console.error(msg) console.error(msg);
logged("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);
} }
} }

View File

@@ -12,14 +12,15 @@ import clide.module 1.0
ApplicationWindow { ApplicationWindow {
id: appWindow id: appWindow
required property string appContextPath
height: 800 height: 800
title: "CLIDE" title: "CLIDE"
visible: true visible: true
width: 1200 width: 1200
required property string appContextPath menuBar: ClideMenuBar {
}
menuBar: ClideMenuBar { }
MessageDialog { MessageDialog {
id: errorDialog id: errorDialog
@@ -28,6 +29,7 @@ ApplicationWindow {
} }
ClideProjectView { ClideProjectView {
id: clideProjectView id: clideProjectView
projectDir: appWindow.appContextPath projectDir: appWindow.appContextPath
} }
} }

View File

@@ -41,6 +41,7 @@ pub mod qobject {
#[qproperty(QColor, debug_log)] #[qproperty(QColor, debug_log)]
#[qproperty(QColor, warn_log)] #[qproperty(QColor, warn_log)]
#[qproperty(QColor, error_log)] #[qproperty(QColor, error_log)]
#[qproperty(QColor, trace_log)]
type RustColors = super::RustColorsImpl; type RustColors = super::RustColorsImpl;
} }
} }
@@ -75,6 +76,7 @@ pub struct RustColorsImpl {
debug_log: QColor, debug_log: QColor,
warn_log: QColor, warn_log: QColor,
error_log: QColor, error_log: QColor,
trace_log: QColor,
} }
impl Default for RustColorsImpl { impl Default for RustColorsImpl {
@@ -104,9 +106,10 @@ impl Default for RustColorsImpl {
explorer_folder_open: QColor::try_from("#2b2b2b").unwrap(), explorer_folder_open: QColor::try_from("#2b2b2b").unwrap(),
terminal_background: QColor::try_from("#111111").unwrap(), terminal_background: QColor::try_from("#111111").unwrap(),
info_log: QColor::try_from("#C4FFFF").unwrap(), info_log: QColor::try_from("#C4FFFF").unwrap(),
debug_log: QColor::try_from("#55ff99").unwrap(), debug_log: QColor::try_from("#9148AF").unwrap(),
warn_log: QColor::try_from("#ffaa00").unwrap(), warn_log: QColor::try_from("#C4A958").unwrap(),
error_log: QColor::try_from("#ff5555").unwrap(), error_log: QColor::try_from("#ff5555").unwrap(),
trace_log: QColor::try_from("#ffaa00").unwrap(),
} }
} }
} }