Add basic GUI support (#17)
This commit was merged in pull request #17.
This commit is contained in:
69
qml/Components/ClideAboutWindow.qml
Normal file
69
qml/Components/ClideAboutWindow.qml
Normal file
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: GNU General Public License v3.0 or later
|
||||
|
||||
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
|
||||
|
||||
// Hide the window when it loses focus.
|
||||
onActiveChanged: {
|
||||
Logger.debug("Setting active: " + root.active)
|
||||
if (!root.active) {
|
||||
root.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Kilroy logo.
|
||||
Image {
|
||||
id: logo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 20
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
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
|
||||
|
||||
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>")
|
||||
textFormat: Text.RichText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
qml/Components/ClideBreadCrumbs.qml
Normal file
107
qml/Components/ClideBreadCrumbs.qml
Normal file
@@ -0,0 +1,107 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
qml/Components/ClideEditor.qml
Normal file
116
qml/Components/ClideEditor.qml
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
qml/Components/ClideHandle.qml
Normal file
44
qml/Components/ClideHandle.qml
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
60
qml/Components/ClideLogger.qml
Normal file
60
qml/Components/ClideLogger.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
14
qml/Components/ClideMenu.qml
Normal file
14
qml/Components/ClideMenu.qml
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
153
qml/Components/ClideMenuBar.qml
Normal file
153
qml/Components/ClideMenuBar.qml
Normal file
@@ -0,0 +1,153 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
qml/Components/ClideMenuItem.qml
Normal file
18
qml/Components/ClideMenuItem.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
74
qml/Components/ClideScrollBar.qml
Normal file
74
qml/Components/ClideScrollBar.qml
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
qml/Components/qmldir
Normal file
9
qml/Components/qmldir
Normal file
@@ -0,0 +1,9 @@
|
||||
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
|
||||
Reference in New Issue
Block a user