feat: Implement core functionality for DTC C/C++ IDE
- Added ProjectManager class to handle project operations including opening, closing, building, and running projects. - Introduced SyntaxHighlighter class for syntax highlighting in C and C++ files. - Developed TerminalWidget for command execution and output display. - Created TextEditor with line numbering and auto-indentation features. - Established main application entry point in main.cpp. - Designed UI layout for MainWindow using Qt Designer.
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Build directories
|
||||
build/
|
||||
dist/
|
||||
bin/
|
||||
|
||||
# Qt creator files
|
||||
*.user
|
||||
*.pro.user
|
||||
|
||||
# Executables
|
||||
*.app
|
||||
*.dmg
|
||||
*.exe
|
||||
|
||||
# OS generated
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# CMake
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
60
CMakeLists.txt
Normal file
60
CMakeLists.txt
Normal file
@@ -0,0 +1,60 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(DTCIde VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Find required Qt6 components
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Gui)
|
||||
|
||||
# Enable Qt MOC, UIC, and RCC
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
# Include directories
|
||||
include_directories(${CMAKE_SOURCE_DIR}/include)
|
||||
|
||||
# Source files
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/MainWindow.cpp
|
||||
src/TextEditor.cpp
|
||||
src/FileTreeWidget.cpp
|
||||
src/TerminalWidget.cpp
|
||||
src/ProjectManager.cpp
|
||||
src/SyntaxHighlighter.cpp
|
||||
)
|
||||
|
||||
# Header files
|
||||
set(HEADERS
|
||||
include/MainWindow.h
|
||||
include/TextEditor.h
|
||||
include/FileTreeWidget.h
|
||||
include/TerminalWidget.h
|
||||
include/ProjectManager.h
|
||||
include/SyntaxHighlighter.h
|
||||
)
|
||||
|
||||
# Create executable
|
||||
add_executable(dtc-ide ${SOURCES} ${HEADERS})
|
||||
|
||||
# Link Qt6 libraries
|
||||
target_link_libraries(dtc-ide Qt6::Core Qt6::Widgets Qt6::Gui)
|
||||
|
||||
# macOS specific settings
|
||||
if(APPLE)
|
||||
set_target_properties(dtc-ide PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "com.dtc.ide"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "DTC C/C++ IDE"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install targets
|
||||
install(TARGETS dtc-ide
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
154
INSTALL.md
Normal file
154
INSTALL.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Hướng dẫn Build và Sử dụng DTC C/C++ IDE
|
||||
|
||||
## Yêu cầu hệ thống
|
||||
|
||||
### macOS
|
||||
- macOS 10.15 (Catalina) trở lên
|
||||
- Xcode Command Line Tools
|
||||
- Qt6 framework
|
||||
- CMake 3.20 trở lên
|
||||
|
||||
## Cài đặt Dependencies
|
||||
|
||||
### 1. Cài đặt Homebrew (nếu chưa có)
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
### 2. Cài đặt Qt6 và CMake
|
||||
```bash
|
||||
# Cài đặt Qt6
|
||||
brew install qt@6
|
||||
|
||||
# Cài đặt CMake
|
||||
brew install cmake
|
||||
|
||||
# Cài đặt Xcode Command Line Tools
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
### 3. Thiết lập biến môi trường (tùy chọn)
|
||||
```bash
|
||||
# Thêm vào ~/.zshrc hoặc ~/.bashrc
|
||||
export CMAKE_PREFIX_PATH="/opt/homebrew/opt/qt@6:$CMAKE_PREFIX_PATH"
|
||||
export PATH="/opt/homebrew/opt/qt@6/bin:$PATH"
|
||||
```
|
||||
|
||||
## Build IDE
|
||||
|
||||
### Cách 1: Sử dụng script tự động
|
||||
```bash
|
||||
# Clone dự án
|
||||
git clone <repository-url>
|
||||
cd ide-c_c++
|
||||
|
||||
# Build
|
||||
./build.sh
|
||||
|
||||
# Chạy
|
||||
./run.sh
|
||||
```
|
||||
|
||||
### Cách 2: Build thủ công
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt@6
|
||||
make -j$(sysctl -n hw.ncpu)
|
||||
```
|
||||
|
||||
## Chạy IDE
|
||||
|
||||
### Từ terminal
|
||||
```bash
|
||||
./build/dtc-ide.app/Contents/MacOS/dtc-ide
|
||||
```
|
||||
|
||||
### Hoặc mở như ứng dụng macOS
|
||||
```bash
|
||||
open build/dtc-ide.app
|
||||
```
|
||||
|
||||
## Tính năng chính
|
||||
|
||||
### Phase 1 (Đã hoàn thành)
|
||||
- ✅ Text editor cơ bản với syntax highlighting
|
||||
- ✅ File tree browser
|
||||
- ✅ Terminal tích hợp
|
||||
- ✅ CMake project detection
|
||||
- ✅ Build system integration
|
||||
- ✅ Dark/Light theme toggle
|
||||
|
||||
### Cách sử dụng
|
||||
|
||||
1. **Mở project**: File → Open Project hoặc Ctrl+Shift+O
|
||||
2. **Tạo file mới**: File → New hoặc Ctrl+N
|
||||
3. **Build project**: Build → Build Project hoặc Ctrl+B
|
||||
4. **Chạy project**: Build → Run hoặc Ctrl+R
|
||||
5. **Terminal**: Sử dụng terminal tích hợp ở dưới cùng
|
||||
6. **Đổi theme**: View → Toggle Theme hoặc Ctrl+T
|
||||
|
||||
### Shortcuts
|
||||
- `Cmd+N`: Tạo file mới
|
||||
- `Cmd+O`: Mở file
|
||||
- `Cmd+S`: Lưu file
|
||||
- `Cmd+Shift+O`: Mở project
|
||||
- `Cmd+B`: Build project
|
||||
- `Cmd+R`: Run project
|
||||
- `Cmd+D`: Debug (Phase 2)
|
||||
- `Cmd+T`: Toggle theme
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Lỗi "Qt6 not found"
|
||||
```bash
|
||||
# Kiểm tra Qt6 đã cài đặt
|
||||
brew list | grep qt
|
||||
|
||||
# Nếu chưa có, cài đặt:
|
||||
brew install qt@6
|
||||
|
||||
# Thiết lập CMAKE_PREFIX_PATH
|
||||
export CMAKE_PREFIX_PATH="/opt/homebrew/opt/qt@6"
|
||||
```
|
||||
|
||||
### Lỗi CMake
|
||||
```bash
|
||||
# Cập nhật CMake
|
||||
brew upgrade cmake
|
||||
|
||||
# Hoặc cài đặt phiên bản mới nhất
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
### Lỗi build
|
||||
```bash
|
||||
# Xóa build directory và build lại
|
||||
rm -rf build
|
||||
./build.sh
|
||||
```
|
||||
|
||||
## Roadmap tiếp theo
|
||||
|
||||
### Phase 2 (Sắp tới)
|
||||
- [ ] LLDB debugger integration
|
||||
- [ ] Code completion với clangd
|
||||
- [ ] Project templates
|
||||
- [ ] Git integration
|
||||
|
||||
### Phase 3 (Tương lai)
|
||||
- [ ] Plugin system
|
||||
- [ ] Code formatting (clang-format)
|
||||
- [ ] Documentation viewer
|
||||
- [ ] Custom keybindings
|
||||
|
||||
## Đóng góp
|
||||
|
||||
1. Fork repository
|
||||
2. Tạo feature branch
|
||||
3. Commit changes
|
||||
4. Push và tạo Pull Request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Xem file LICENSE để biết thêm chi tiết.
|
135
PROJECT_SUMMARY.md
Normal file
135
PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 🎉 DTC C/C++ IDE for macOS - Hoàn thành!
|
||||
|
||||
## ✅ Những gì đã được tạo ra
|
||||
|
||||
### 1. IDE Core Application (Phase 1 - MVP)
|
||||
**Địa chỉ**: `/Users/dothanh1110/DTC/ide-cpp/`
|
||||
|
||||
**Tính năng đã hoàn thành**:
|
||||
- ✅ **Text Editor** với syntax highlighting cho C/C++
|
||||
- ✅ **File Tree Browser** để duyệt file và thư mục
|
||||
- ✅ **Terminal tích hợp** với zsh shell cho macOS
|
||||
- ✅ **Project Manager** hỗ trợ CMake projects
|
||||
- ✅ **Build System** tích hợp (Cmd+B để build)
|
||||
- ✅ **Run System** tích hợp (Cmd+R để chạy)
|
||||
- ✅ **Dark/Light Theme** toggle (Cmd+T)
|
||||
- ✅ **Auto-save** và line numbers
|
||||
- ✅ **macOS app bundle** (.app file)
|
||||
|
||||
### 2. Build System
|
||||
- **build.sh**: Script tự động build với Qt6 detection
|
||||
- **run.sh**: Script chạy IDE
|
||||
- **CMakeLists.txt**: Cấu hình CMake cho Qt6
|
||||
|
||||
### 3. Sample Project
|
||||
**Địa chỉ**: `/Users/dothanh1110/DTC/sample-cpp-project/`
|
||||
- Calculator app với C++20
|
||||
- CMake configuration
|
||||
- Sẵn sàng để test với IDE
|
||||
|
||||
## 🚀 Cách sử dụng
|
||||
|
||||
### Chạy IDE:
|
||||
```bash
|
||||
cd /Users/dothanh1110/DTC/ide-cpp
|
||||
./run.sh
|
||||
```
|
||||
|
||||
### Test với Sample Project:
|
||||
1. Chạy IDE
|
||||
2. File → Open Project → chọn `/Users/dothanh1110/DTC/sample-cpp-project`
|
||||
3. Cmd+B để build
|
||||
4. Cmd+R để run
|
||||
|
||||
### Keyboard Shortcuts:
|
||||
- **Cmd+N**: New file
|
||||
- **Cmd+O**: Open file
|
||||
- **Cmd+S**: Save file
|
||||
- **Cmd+Shift+O**: Open project
|
||||
- **Cmd+B**: Build project
|
||||
- **Cmd+R**: Run project
|
||||
- **Cmd+T**: Toggle theme
|
||||
|
||||
## 🏗️ Kiến trúc
|
||||
|
||||
### Core Components:
|
||||
1. **MainWindow**: UI chính, menu, toolbar
|
||||
2. **TextEditor**: Editor với syntax highlighting và line numbers
|
||||
3. **FileTreeWidget**: File browser với context menu
|
||||
4. **TerminalWidget**: Terminal emulator tích hợp
|
||||
5. **ProjectManager**: Quản lý CMake projects
|
||||
6. **SyntaxHighlighter**: C/C++ syntax highlighting
|
||||
|
||||
### Dependencies:
|
||||
- **Qt6**: GUI framework
|
||||
- **CMake**: Build system
|
||||
- **macOS**: 10.15+ (Catalina trở lên)
|
||||
|
||||
## 📁 Cấu trúc Project
|
||||
|
||||
```
|
||||
ide-cpp/
|
||||
├── src/ # Source files
|
||||
├── include/ # Header files
|
||||
├── build/ # Build output
|
||||
├── build.sh # Build script
|
||||
├── run.sh # Run script
|
||||
├── CMakeLists.txt # CMake config
|
||||
├── README.md # Documentation
|
||||
├── INSTALL.md # Installation guide
|
||||
└── create-sample-project.sh
|
||||
```
|
||||
|
||||
## 🔮 Roadmap (Phases tiếp theo)
|
||||
|
||||
### Phase 2 - Advanced Features:
|
||||
- [ ] **LLDB Debugger** integration
|
||||
- [ ] **clangd** code completion
|
||||
- [ ] **Git integration**
|
||||
- [ ] **Project templates**
|
||||
|
||||
### Phase 3 - Polish:
|
||||
- [ ] **Plugin system**
|
||||
- [ ] **clang-format** integration
|
||||
- [ ] **Custom keybindings**
|
||||
- [ ] **Documentation viewer**
|
||||
|
||||
## 💡 Technical Highlights
|
||||
|
||||
1. **Modern C++20**: Sử dụng C++20 features
|
||||
2. **Qt6 Native**: Tích hợp đầy đủ với Qt6 framework
|
||||
3. **macOS Optimized**: App bundle, native look & feel
|
||||
4. **CMake Support**: Tự động detect và build CMake projects
|
||||
5. **Real-time Compilation**: Build và run trực tiếp từ IDE
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Nếu IDE không chạy:
|
||||
```bash
|
||||
# Kiểm tra Qt6
|
||||
brew list | grep qt
|
||||
|
||||
# Re-install Qt6 nếu cần
|
||||
brew reinstall qt@6
|
||||
|
||||
# Re-build IDE
|
||||
cd /Users/dothanh1110/DTC/ide-cpp
|
||||
rm -rf build
|
||||
./build.sh
|
||||
```
|
||||
|
||||
### Nếu build project failed:
|
||||
- Kiểm tra CMakeLists.txt trong project
|
||||
- Đảm bảo có compiler (clang++)
|
||||
- Xem terminal output để debug
|
||||
|
||||
## 🎯 Kết luận
|
||||
|
||||
**DTC C/C++ IDE v0.1.0** đã hoàn thành Phase 1 với đầy đủ tính năng cơ bản:
|
||||
- ✅ Text editing với syntax highlighting
|
||||
- ✅ Project management
|
||||
- ✅ Build & run system
|
||||
- ✅ Integrated terminal
|
||||
- ✅ Modern GUI
|
||||
|
||||
IDE sẵn sàng để sử dụng cho development C/C++ trên macOS! 🚀
|
70
build.sh
Executable file
70
build.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for DTC C/C++ IDE on macOS
|
||||
|
||||
echo "🚀 Building DTC C/C++ IDE for macOS..."
|
||||
|
||||
# Check if Qt6 is installed
|
||||
if ! command -v qmake6 &> /dev/null && ! command -v qmake &> /dev/null; then
|
||||
echo "❌ Qt6 is not installed. Please install Qt6 first:"
|
||||
echo " brew install qt@6"
|
||||
echo " or download from https://qt.io"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find Qt6 installation path
|
||||
QT_PATH=""
|
||||
if [ -d "/opt/homebrew/opt/qt@6" ]; then
|
||||
QT_PATH="/opt/homebrew/opt/qt@6"
|
||||
elif [ -d "/usr/local/opt/qt@6" ]; then
|
||||
QT_PATH="/usr/local/opt/qt@6"
|
||||
elif [ -d "$HOME/Qt" ]; then
|
||||
# Find latest Qt version
|
||||
QT_VERSION=$(ls "$HOME/Qt" | grep "^6\." | sort -V | tail -n1)
|
||||
if [ ! -z "$QT_VERSION" ]; then
|
||||
QT_PATH="$HOME/Qt/$QT_VERSION/macos"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$QT_PATH" ]; then
|
||||
echo "❌ Could not find Qt6 installation. Please set CMAKE_PREFIX_PATH manually."
|
||||
echo "Example: export CMAKE_PREFIX_PATH=/path/to/qt6"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Found Qt6 at: $QT_PATH"
|
||||
|
||||
# Create build directory
|
||||
if [ ! -d "build" ]; then
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
cd build
|
||||
|
||||
# Configure with CMake
|
||||
echo "🔧 Configuring project..."
|
||||
cmake .. -DCMAKE_PREFIX_PATH="$QT_PATH" -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ CMake configuration failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the project
|
||||
echo "🔨 Building project..."
|
||||
make -j$(sysctl -n hw.ncpu)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Build completed successfully!"
|
||||
echo ""
|
||||
echo "🎉 DTC C/C++ IDE has been built!"
|
||||
echo "📁 Executable location: $(pwd)/dtc-ide.app"
|
||||
echo ""
|
||||
echo "To run the IDE:"
|
||||
echo " ./dtc-ide.app/Contents/MacOS/dtc-ide"
|
||||
echo " or"
|
||||
echo " open dtc-ide.app"
|
206
create-sample-project.sh
Executable file
206
create-sample-project.sh
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🎯 Creating C++ sample project for testing DTC IDE..."
|
||||
|
||||
# Create sample project directory
|
||||
mkdir -p /Users/dothanh1110/DTC/sample-cpp-project
|
||||
|
||||
cd /Users/dothanh1110/DTC/sample-cpp-project
|
||||
|
||||
# Create CMakeLists.txt
|
||||
cat > CMakeLists.txt << 'EOF'
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(SampleApp VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Add executable
|
||||
add_executable(sample-app
|
||||
src/main.cpp
|
||||
src/Calculator.cpp
|
||||
)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(sample-app PRIVATE include)
|
||||
|
||||
# Install
|
||||
install(TARGETS sample-app DESTINATION bin)
|
||||
EOF
|
||||
|
||||
# Create directories
|
||||
mkdir -p src include
|
||||
|
||||
# Create main.cpp
|
||||
cat > src/main.cpp << 'EOF'
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "Calculator.h"
|
||||
|
||||
int main() {
|
||||
std::cout << "🧮 Welcome to Sample Calculator!" << std::endl;
|
||||
|
||||
Calculator calc;
|
||||
|
||||
// Test basic operations
|
||||
std::cout << "Testing basic operations:" << std::endl;
|
||||
std::cout << "5 + 3 = " << calc.add(5, 3) << std::endl;
|
||||
std::cout << "10 - 4 = " << calc.subtract(10, 4) << std::endl;
|
||||
std::cout << "6 * 7 = " << calc.multiply(6, 7) << std::endl;
|
||||
std::cout << "15 / 3 = " << calc.divide(15, 3) << std::endl;
|
||||
|
||||
// Interactive mode
|
||||
std::cout << "\n📱 Interactive Calculator (type 'quit' to exit):" << std::endl;
|
||||
|
||||
double a, b;
|
||||
std::string operation;
|
||||
|
||||
while (true) {
|
||||
std::cout << "Enter operation (add/sub/mul/div) or 'quit': ";
|
||||
std::cin >> operation;
|
||||
|
||||
if (operation == "quit") {
|
||||
break;
|
||||
}
|
||||
|
||||
std::cout << "Enter first number: ";
|
||||
std::cin >> a;
|
||||
std::cout << "Enter second number: ";
|
||||
std::cin >> b;
|
||||
|
||||
if (operation == "add") {
|
||||
std::cout << "Result: " << calc.add(a, b) << std::endl;
|
||||
} else if (operation == "sub") {
|
||||
std::cout << "Result: " << calc.subtract(a, b) << std::endl;
|
||||
} else if (operation == "mul") {
|
||||
std::cout << "Result: " << calc.multiply(a, b) << std::endl;
|
||||
} else if (operation == "div") {
|
||||
std::cout << "Result: " << calc.divide(a, b) << std::endl;
|
||||
} else {
|
||||
std::cout << "Unknown operation!" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "👋 Goodbye!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create Calculator.h
|
||||
cat > include/Calculator.h << 'EOF'
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @brief Simple Calculator class for basic arithmetic operations
|
||||
*/
|
||||
class Calculator {
|
||||
public:
|
||||
/**
|
||||
* @brief Add two numbers
|
||||
* @param a First number
|
||||
* @param b Second number
|
||||
* @return Sum of a and b
|
||||
*/
|
||||
double add(double a, double b);
|
||||
|
||||
/**
|
||||
* @brief Subtract two numbers
|
||||
* @param a First number
|
||||
* @param b Second number
|
||||
* @return Difference of a and b
|
||||
*/
|
||||
double subtract(double a, double b);
|
||||
|
||||
/**
|
||||
* @brief Multiply two numbers
|
||||
* @param a First number
|
||||
* @param b Second number
|
||||
* @return Product of a and b
|
||||
*/
|
||||
double multiply(double a, double b);
|
||||
|
||||
/**
|
||||
* @brief Divide two numbers
|
||||
* @param a Dividend
|
||||
* @param b Divisor
|
||||
* @return Quotient of a and b
|
||||
* @throws std::invalid_argument if b is zero
|
||||
*/
|
||||
double divide(double a, double b);
|
||||
|
||||
private:
|
||||
static constexpr double EPSILON = 1e-9;
|
||||
};
|
||||
EOF
|
||||
|
||||
# Create Calculator.cpp
|
||||
cat > src/Calculator.cpp << 'EOF'
|
||||
#include "Calculator.h"
|
||||
#include <stdexcept>
|
||||
#include <cmath>
|
||||
|
||||
double Calculator::add(double a, double b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
double Calculator::subtract(double a, double b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
double Calculator::multiply(double a, double b) {
|
||||
return a * b;
|
||||
}
|
||||
|
||||
double Calculator::divide(double a, double b) {
|
||||
if (std::abs(b) < EPSILON) {
|
||||
throw std::invalid_argument("Division by zero is not allowed!");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create README.md
|
||||
cat > README.md << 'EOF'
|
||||
# Sample C++ Calculator Project
|
||||
|
||||
This is a sample C++ project for testing the DTC C/C++ IDE.
|
||||
|
||||
## Features
|
||||
- Basic arithmetic operations (add, subtract, multiply, divide)
|
||||
- Interactive calculator mode
|
||||
- Error handling for division by zero
|
||||
- Modern C++20 features
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
./sample-app
|
||||
```
|
||||
|
||||
## Testing in DTC IDE
|
||||
|
||||
1. Open DTC C/C++ IDE
|
||||
2. File → Open Project
|
||||
3. Select this directory
|
||||
4. Use Cmd+B to build
|
||||
5. Use Cmd+R to run
|
||||
EOF
|
||||
|
||||
echo "✅ Sample C++ project created at: /Users/dothanh1110/DTC/sample-cpp-project"
|
||||
echo ""
|
||||
echo "📖 To test with DTC IDE:"
|
||||
echo " 1. Open the IDE: cd /Users/dothanh1110/DTC/ide-cpp && ./run.sh"
|
||||
echo " 2. File → Open Project"
|
||||
echo " 3. Select: /Users/dothanh1110/DTC/sample-cpp-project"
|
||||
echo " 4. Build: Cmd+B"
|
||||
echo " 5. Run: Cmd+R"
|
47
include/FileTreeWidget.h
Normal file
47
include/FileTreeWidget.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtWidgets/QTreeWidget>
|
||||
#include <QtWidgets/QTreeWidgetItem>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtCore/QFileSystemWatcher>
|
||||
#include <QtCore/QDir>
|
||||
|
||||
class FileTreeWidget : public QTreeWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FileTreeWidget(QWidget *parent = nullptr);
|
||||
~FileTreeWidget();
|
||||
|
||||
void setRootPath(const QString &path);
|
||||
QString rootPath() const;
|
||||
|
||||
void refreshTree();
|
||||
|
||||
signals:
|
||||
void fileSelected(const QString &filePath);
|
||||
void fileDoubleClicked(const QString &filePath);
|
||||
void directorySelected(const QString &dirPath);
|
||||
|
||||
private slots:
|
||||
void onItemClicked(QTreeWidgetItem *item, int column);
|
||||
void onItemDoubleClicked(QTreeWidgetItem *item, int column);
|
||||
void onCustomContextMenu(const QPoint &point);
|
||||
void onDirectoryChanged(const QString &path);
|
||||
void newFile();
|
||||
void newFolder();
|
||||
void deleteItem();
|
||||
void renameItem();
|
||||
|
||||
private:
|
||||
void populateTree(const QString &path, QTreeWidgetItem *parent = nullptr);
|
||||
void setupContextMenu();
|
||||
QString getItemPath(QTreeWidgetItem *item) const;
|
||||
bool isDirectory(QTreeWidgetItem *item) const;
|
||||
|
||||
QString m_rootPath;
|
||||
QFileSystemWatcher *m_fileWatcher;
|
||||
QMenu *m_contextMenu;
|
||||
QTreeWidgetItem *m_contextItem;
|
||||
};
|
65
include/MainWindow.h
Normal file
65
include/MainWindow.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QtWidgets/QMenuBar>
|
||||
#include <QtWidgets/QToolBar>
|
||||
#include <QtWidgets/QStatusBar>
|
||||
#include <QtWidgets/QSplitter>
|
||||
#include <QtWidgets/QTabWidget>
|
||||
#include <QtWidgets/QVBoxLayout>
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtCore/QSettings>
|
||||
|
||||
class TextEditor;
|
||||
class FileTreeWidget;
|
||||
class TerminalWidget;
|
||||
class ProjectManager;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
private slots:
|
||||
void newFile();
|
||||
void openFile();
|
||||
void saveFile();
|
||||
void saveAsFile();
|
||||
void openProject();
|
||||
void buildProject();
|
||||
void runProject();
|
||||
void debugProject();
|
||||
void showAbout();
|
||||
void toggleTheme();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupMenus();
|
||||
void setupToolbar();
|
||||
void setupStatusBar();
|
||||
void connectSignals();
|
||||
void loadSettings();
|
||||
void saveSettings();
|
||||
|
||||
// UI Components
|
||||
QSplitter *m_centralSplitter;
|
||||
QSplitter *m_leftSplitter;
|
||||
QTabWidget *m_editorTabs;
|
||||
FileTreeWidget *m_fileTree;
|
||||
TerminalWidget *m_terminal;
|
||||
|
||||
// Managers
|
||||
ProjectManager *m_projectManager;
|
||||
|
||||
// Settings
|
||||
QSettings *m_settings;
|
||||
bool m_darkTheme;
|
||||
|
||||
// Current file
|
||||
QString m_currentFilePath;
|
||||
};
|
66
include/ProjectManager.h
Normal file
66
include/ProjectManager.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QProcess>
|
||||
|
||||
class ProjectManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProjectManager(QObject *parent = nullptr);
|
||||
~ProjectManager();
|
||||
|
||||
bool openProject(const QString &projectPath);
|
||||
void closeProject();
|
||||
|
||||
QString projectPath() const;
|
||||
QString projectName() const;
|
||||
bool hasProject() const;
|
||||
|
||||
bool isCMakeProject() const;
|
||||
bool isQMakeProject() const;
|
||||
|
||||
void buildProject();
|
||||
void cleanProject();
|
||||
void configureProject();
|
||||
void runProject();
|
||||
|
||||
QString buildDirectory() const;
|
||||
QString sourceDirectory() const;
|
||||
|
||||
signals:
|
||||
void projectOpened(const QString &projectPath);
|
||||
void projectClosed();
|
||||
void buildStarted();
|
||||
void buildFinished(bool success);
|
||||
void buildOutput(const QString &output);
|
||||
|
||||
private slots:
|
||||
void onBuildProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||
void onBuildProcessError(QProcess::ProcessError error);
|
||||
void onBuildOutput();
|
||||
|
||||
private:
|
||||
void detectProjectType();
|
||||
void setupBuildDirectory();
|
||||
QString findCMakeListsFile(const QString &directory) const;
|
||||
QString findQMakeFile(const QString &directory) const;
|
||||
|
||||
QString m_projectPath;
|
||||
QString m_projectName;
|
||||
QString m_buildDirectory;
|
||||
|
||||
enum ProjectType {
|
||||
Unknown,
|
||||
CMake,
|
||||
QMake
|
||||
};
|
||||
ProjectType m_projectType;
|
||||
|
||||
QProcess *m_buildProcess;
|
||||
bool m_isBuilding;
|
||||
};
|
47
include/SyntaxHighlighter.h
Normal file
47
include/SyntaxHighlighter.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtGui/QSyntaxHighlighter>
|
||||
#include <QtGui/QTextDocument>
|
||||
#include <QtGui/QTextCharFormat>
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
class SyntaxHighlighter : public QSyntaxHighlighter
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SyntaxHighlighter(QTextDocument *parent = nullptr);
|
||||
~SyntaxHighlighter();
|
||||
|
||||
void setLanguage(const QString &language);
|
||||
QString language() const;
|
||||
|
||||
protected:
|
||||
void highlightBlock(const QString &text) override;
|
||||
|
||||
private:
|
||||
struct HighlightingRule
|
||||
{
|
||||
QRegularExpression pattern;
|
||||
QTextCharFormat format;
|
||||
};
|
||||
QVector<HighlightingRule> m_highlightingRules;
|
||||
|
||||
void setupCppHighlighting();
|
||||
void setupCHighlighting();
|
||||
void setupCommonRules();
|
||||
|
||||
QTextCharFormat m_keywordFormat;
|
||||
QTextCharFormat m_classFormat;
|
||||
QTextCharFormat m_singleLineCommentFormat;
|
||||
QTextCharFormat m_multiLineCommentFormat;
|
||||
QTextCharFormat m_quotationFormat;
|
||||
QTextCharFormat m_functionFormat;
|
||||
QTextCharFormat m_numberFormat;
|
||||
QTextCharFormat m_preprocessorFormat;
|
||||
|
||||
QRegularExpression m_commentStartExpression;
|
||||
QRegularExpression m_commentEndExpression;
|
||||
|
||||
QString m_language;
|
||||
};
|
58
include/TerminalWidget.h
Normal file
58
include/TerminalWidget.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtWidgets/QWidget>
|
||||
#include <QtWidgets/QTextEdit>
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtWidgets/QVBoxLayout>
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
#include <QtWidgets/QPushButton>
|
||||
#include <QtCore/QProcess>
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
class TerminalWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TerminalWidget(QWidget *parent = nullptr);
|
||||
~TerminalWidget();
|
||||
|
||||
void setWorkingDirectory(const QString &directory);
|
||||
QString workingDirectory() const;
|
||||
|
||||
void executeCommand(const QString &command);
|
||||
void clear();
|
||||
|
||||
void showBuildOutput();
|
||||
void showRunOutput();
|
||||
|
||||
signals:
|
||||
void commandFinished(int exitCode);
|
||||
|
||||
private slots:
|
||||
void onCommandEntered();
|
||||
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||
void onProcessError(QProcess::ProcessError error);
|
||||
void onProcessOutput();
|
||||
void updatePrompt();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupProcess();
|
||||
void appendOutput(const QString &text, const QColor &color = Qt::white);
|
||||
void appendPrompt();
|
||||
|
||||
QTextEdit *m_output;
|
||||
QLineEdit *m_input;
|
||||
QPushButton *m_clearButton;
|
||||
|
||||
QProcess *m_process;
|
||||
QString m_workingDirectory;
|
||||
QString m_currentPrompt;
|
||||
|
||||
// Colors for different output types
|
||||
QColor m_normalColor;
|
||||
QColor m_errorColor;
|
||||
QColor m_successColor;
|
||||
QColor m_promptColor;
|
||||
};
|
77
include/TextEditor.h
Normal file
77
include/TextEditor.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtWidgets/QPlainTextEdit>
|
||||
#include <QtCore/QTextStream>
|
||||
#include <QtGui/QTextDocument>
|
||||
#include <QtGui/QTextCursor>
|
||||
#include <QtGui/QKeyEvent>
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
class SyntaxHighlighter;
|
||||
|
||||
class TextEditor : public QPlainTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
friend class LineNumberArea;
|
||||
|
||||
public:
|
||||
explicit TextEditor(QWidget *parent = nullptr);
|
||||
~TextEditor();
|
||||
|
||||
void setFilePath(const QString &filePath);
|
||||
QString filePath() const;
|
||||
|
||||
bool isModified() const;
|
||||
void setModified(bool modified);
|
||||
|
||||
bool loadFile(const QString &filePath);
|
||||
bool saveFile();
|
||||
bool saveAsFile(const QString &filePath);
|
||||
|
||||
void setLineNumberAreaWidth(int width);
|
||||
void updateLineNumberArea(const QRect &rect, int dy);
|
||||
|
||||
signals:
|
||||
void fileModified(bool modified);
|
||||
void cursorPositionChanged(int line, int column);
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void highlightCurrentLine();
|
||||
void updateCursorPosition();
|
||||
void autoIndent();
|
||||
|
||||
private:
|
||||
void setupEditor();
|
||||
void updateLineNumberAreaWidth(int newBlockCount);
|
||||
|
||||
public:
|
||||
int lineNumberAreaWidth();
|
||||
|
||||
QString m_filePath;
|
||||
bool m_isModified;
|
||||
SyntaxHighlighter *m_syntaxHighlighter;
|
||||
QWidget *m_lineNumberArea;
|
||||
QTimer *m_autoSaveTimer;
|
||||
};
|
||||
|
||||
// Line number area widget
|
||||
class LineNumberArea : public QWidget
|
||||
{
|
||||
public:
|
||||
LineNumberArea(TextEditor *editor) : QWidget(editor), m_textEditor(editor) {}
|
||||
|
||||
QSize sizeHint() const override {
|
||||
return QSize(m_textEditor->lineNumberAreaWidth(), 0);
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
TextEditor *m_textEditor;
|
||||
};
|
33
plan.md
Normal file
33
plan.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Development Roadmap
|
||||
|
||||
## Phase 1: Core Features
|
||||
- [ ] Create basic text editor component
|
||||
- [ ] Syntax highlighting using ANTLR grammar
|
||||
- [ ] File tree browser
|
||||
- [ ] Integrated terminal emulator
|
||||
- [ ] CMake project detection
|
||||
|
||||
## Phase 2: Advanced Features
|
||||
- [ ] LLDB debugger integration
|
||||
- [ ] Autocomplete using clang-tidy
|
||||
- [ ] Project templates
|
||||
- [ ] Dark/Light theme switching
|
||||
- [ ] Git integration basics
|
||||
|
||||
## Phase 3: Polish
|
||||
- [ ] Code formatting (clang-format integration)
|
||||
- [ ] Documentation viewer
|
||||
- [ ] Customizable keybindings
|
||||
- [ ] Plugin system architecture
|
||||
- [ ] Performance optimization
|
||||
|
||||
## Milestones
|
||||
1. MVP (v0.1) - Basic editor + build system
|
||||
2. Debug Preview (v0.5) - Integrated debugging
|
||||
3. Release Candidate (v1.0) - Full feature set
|
||||
|
||||
## Development Environment
|
||||
- Primary Language: C++20
|
||||
- GUI Framework: Qt 6
|
||||
- Build System: CMake
|
||||
- Continuous Integration: GitHub Actions
|
21
run.sh
Executable file
21
run.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run script for DTC C/C++ IDE
|
||||
|
||||
echo "🚀 Starting DTC C/C++ IDE..."
|
||||
|
||||
# Check if built
|
||||
if [ ! -f "build/dtc-ide.app/Contents/MacOS/dtc-ide" ] && [ ! -f "build/dtc-ide" ]; then
|
||||
echo "❌ IDE not built yet. Please run ./build.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run the IDE
|
||||
if [ -f "build/dtc-ide.app/Contents/MacOS/dtc-ide" ]; then
|
||||
echo "📱 Running macOS app bundle..."
|
||||
open build/dtc-ide.app
|
||||
elif [ -f "build/dtc-ide" ]; then
|
||||
echo "🖥️ Running executable..."
|
||||
cd build
|
||||
./dtc-ide
|
||||
fi
|
360
src/FileTreeWidget.cpp
Normal file
360
src/FileTreeWidget.cpp
Normal file
@@ -0,0 +1,360 @@
|
||||
#include "FileTreeWidget.h"
|
||||
|
||||
#include <QtWidgets/QHeaderView>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtGui/QAction>
|
||||
#include <QtWidgets/QInputDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QMimeData>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtGui/QContextMenuEvent>
|
||||
#include <QtGui/QPalette>
|
||||
#include <QtGui/QFont>
|
||||
#include <QtWidgets/QLabel>
|
||||
|
||||
FileTreeWidget::FileTreeWidget(QWidget *parent)
|
||||
: QTreeWidget(parent)
|
||||
, m_fileWatcher(nullptr)
|
||||
, m_contextMenu(nullptr)
|
||||
, m_contextItem(nullptr)
|
||||
{
|
||||
setHeaderHidden(true);
|
||||
setRootIsDecorated(true);
|
||||
setAlternatingRowColors(false); // VS Code style: no alternating rows
|
||||
setDragDropMode(QAbstractItemView::InternalMove);
|
||||
setIndentation(18); // VS Code style
|
||||
setStyleSheet(R"(
|
||||
QTreeWidget {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border: none;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
QTreeWidget::item {
|
||||
height: 28px;
|
||||
padding-left: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QTreeWidget::item:selected {
|
||||
background: #2a2d2e;
|
||||
color: #ffffff;
|
||||
}
|
||||
QTreeWidget::item:hover {
|
||||
background: #23272e;
|
||||
}
|
||||
QTreeWidget::branch:has-children:!has-siblings:closed,
|
||||
QTreeWidget::branch:closed:has-children:has-siblings {
|
||||
border-image: none;
|
||||
image: url(:/vsc/arrow-right.svg);
|
||||
}
|
||||
QTreeWidget::branch:open:has-children:!has-siblings,
|
||||
QTreeWidget::branch:open:has-children:has-siblings {
|
||||
border-image: none;
|
||||
image: url(:/vsc/arrow-down.svg);
|
||||
}
|
||||
)");
|
||||
// Add Explorer header like VS Code
|
||||
QLabel *explorerHeader = new QLabel("EXPLORER", this);
|
||||
explorerHeader->setStyleSheet("color: #858585; background: #23272e; font-weight: bold; padding: 8px 8px 4px 8px; letter-spacing: 1px;");
|
||||
explorerHeader->setFixedHeight(28);
|
||||
explorerHeader->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
explorerHeader->setFont(QFont("JetBrains Mono", 11, QFont::Bold));
|
||||
explorerHeader->move(0, 0);
|
||||
explorerHeader->raise();
|
||||
setViewportMargins(0, 28, 0, 0);
|
||||
|
||||
// Setup file watcher
|
||||
m_fileWatcher = new QFileSystemWatcher(this);
|
||||
connect(m_fileWatcher, &QFileSystemWatcher::directoryChanged,
|
||||
this, &FileTreeWidget::onDirectoryChanged);
|
||||
|
||||
// Setup context menu
|
||||
setupContextMenu();
|
||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(this, &QTreeWidget::customContextMenuRequested,
|
||||
this, &FileTreeWidget::onCustomContextMenu);
|
||||
|
||||
// Connect signals
|
||||
connect(this, &QTreeWidget::itemClicked,
|
||||
this, &FileTreeWidget::onItemClicked);
|
||||
connect(this, &QTreeWidget::itemDoubleClicked,
|
||||
this, &FileTreeWidget::onItemDoubleClicked);
|
||||
}
|
||||
|
||||
FileTreeWidget::~FileTreeWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void FileTreeWidget::setRootPath(const QString &path)
|
||||
{
|
||||
if (m_rootPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_rootPath = path;
|
||||
clear();
|
||||
|
||||
if (!path.isEmpty() && QDir(path).exists()) {
|
||||
populateTree(path);
|
||||
|
||||
// Watch the root directory
|
||||
if (m_fileWatcher) {
|
||||
m_fileWatcher->removePaths(m_fileWatcher->directories());
|
||||
m_fileWatcher->addPath(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString FileTreeWidget::rootPath() const
|
||||
{
|
||||
return m_rootPath;
|
||||
}
|
||||
|
||||
void FileTreeWidget::refreshTree()
|
||||
{
|
||||
setRootPath(m_rootPath);
|
||||
}
|
||||
|
||||
void FileTreeWidget::populateTree(const QString &path, QTreeWidgetItem *parent)
|
||||
{
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get directory entries
|
||||
QFileInfoList entries = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot,
|
||||
QDir::DirsFirst | QDir::Name);
|
||||
|
||||
for (const QFileInfo &entry : entries) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem();
|
||||
item->setText(0, entry.fileName());
|
||||
item->setData(0, Qt::UserRole, entry.absoluteFilePath());
|
||||
|
||||
if (entry.isDir()) {
|
||||
item->setData(0, Qt::UserRole + 1, true); // isDirectory flag
|
||||
item->setIcon(0, style()->standardIcon(QStyle::SP_DirIcon));
|
||||
|
||||
// Add placeholder child for expandable directories
|
||||
QTreeWidgetItem *placeholder = new QTreeWidgetItem();
|
||||
placeholder->setText(0, "Loading...");
|
||||
item->addChild(placeholder);
|
||||
|
||||
// Watch directory for changes
|
||||
if (m_fileWatcher) {
|
||||
m_fileWatcher->addPath(entry.absoluteFilePath());
|
||||
}
|
||||
} else {
|
||||
item->setData(0, Qt::UserRole + 1, false); // isDirectory flag
|
||||
item->setIcon(0, style()->standardIcon(QStyle::SP_FileIcon));
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent->addChild(item);
|
||||
} else {
|
||||
addTopLevelItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeWidget::setupContextMenu()
|
||||
{
|
||||
m_contextMenu = new QMenu(this);
|
||||
|
||||
QAction *newFileAction = m_contextMenu->addAction("New File", this, &FileTreeWidget::newFile);
|
||||
QAction *newFolderAction = m_contextMenu->addAction("New Folder", this, &FileTreeWidget::newFolder);
|
||||
m_contextMenu->addSeparator();
|
||||
QAction *deleteAction = m_contextMenu->addAction("Delete", this, &FileTreeWidget::deleteItem);
|
||||
QAction *renameAction = m_contextMenu->addAction("Rename", this, &FileTreeWidget::renameItem);
|
||||
}
|
||||
|
||||
QString FileTreeWidget::getItemPath(QTreeWidgetItem *item) const
|
||||
{
|
||||
if (!item) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return item->data(0, Qt::UserRole).toString();
|
||||
}
|
||||
|
||||
bool FileTreeWidget::isDirectory(QTreeWidgetItem *item) const
|
||||
{
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item->data(0, Qt::UserRole + 1).toBool();
|
||||
}
|
||||
|
||||
void FileTreeWidget::onItemClicked(QTreeWidgetItem *item, int column)
|
||||
{
|
||||
Q_UNUSED(column)
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString path = getItemPath(item);
|
||||
|
||||
if (isDirectory(item)) {
|
||||
emit directorySelected(path);
|
||||
|
||||
// Expand directory and load children if needed
|
||||
if (!item->isExpanded() && item->childCount() == 1 &&
|
||||
item->child(0)->text(0) == "Loading...") {
|
||||
|
||||
// Remove placeholder
|
||||
delete item->takeChild(0);
|
||||
|
||||
// Populate directory
|
||||
populateTree(path, item);
|
||||
}
|
||||
} else {
|
||||
emit fileSelected(path);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeWidget::onItemDoubleClicked(QTreeWidgetItem *item, int column)
|
||||
{
|
||||
Q_UNUSED(column)
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString path = getItemPath(item);
|
||||
|
||||
if (!isDirectory(item)) {
|
||||
emit fileDoubleClicked(path);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeWidget::onCustomContextMenu(const QPoint &point)
|
||||
{
|
||||
m_contextItem = itemAt(point);
|
||||
|
||||
if (m_contextMenu) {
|
||||
m_contextMenu->exec(mapToGlobal(point));
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeWidget::onDirectoryChanged(const QString &path)
|
||||
{
|
||||
Q_UNUSED(path)
|
||||
// Refresh tree when directory changes
|
||||
QTimer::singleShot(100, this, &FileTreeWidget::refreshTree);
|
||||
}
|
||||
|
||||
void FileTreeWidget::newFile()
|
||||
{
|
||||
QString dirPath = m_rootPath;
|
||||
|
||||
if (m_contextItem) {
|
||||
QString itemPath = getItemPath(m_contextItem);
|
||||
if (isDirectory(m_contextItem)) {
|
||||
dirPath = itemPath;
|
||||
} else {
|
||||
dirPath = QFileInfo(itemPath).absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString fileName = QInputDialog::getText(this, "New File", "File name:", QLineEdit::Normal, QString(), &ok);
|
||||
|
||||
if (ok && !fileName.isEmpty()) {
|
||||
QString filePath = QDir(dirPath).absoluteFilePath(fileName);
|
||||
QFile file(filePath);
|
||||
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.close();
|
||||
refreshTree();
|
||||
} else {
|
||||
QMessageBox::warning(this, "Error", "Could not create file: " + fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeWidget::newFolder()
|
||||
{
|
||||
QString dirPath = m_rootPath;
|
||||
|
||||
if (m_contextItem) {
|
||||
QString itemPath = getItemPath(m_contextItem);
|
||||
if (isDirectory(m_contextItem)) {
|
||||
dirPath = itemPath;
|
||||
} else {
|
||||
dirPath = QFileInfo(itemPath).absolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString folderName = QInputDialog::getText(this, "New Folder", "Folder name:", QLineEdit::Normal, QString(), &ok);
|
||||
|
||||
if (ok && !folderName.isEmpty()) {
|
||||
QString folderPath = QDir(dirPath).absoluteFilePath(folderName);
|
||||
QDir dir;
|
||||
|
||||
if (dir.mkpath(folderPath)) {
|
||||
refreshTree();
|
||||
} else {
|
||||
QMessageBox::warning(this, "Error", "Could not create folder: " + folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeWidget::deleteItem()
|
||||
{
|
||||
if (!m_contextItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString itemPath = getItemPath(m_contextItem);
|
||||
QString itemName = m_contextItem->text(0);
|
||||
|
||||
int ret = QMessageBox::question(this, "Delete",
|
||||
QString("Are you sure you want to delete '%1'?").arg(itemName),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (ret == QMessageBox::Yes) {
|
||||
if (isDirectory(m_contextItem)) {
|
||||
QDir dir(itemPath);
|
||||
if (dir.removeRecursively()) {
|
||||
refreshTree();
|
||||
} else {
|
||||
QMessageBox::warning(this, "Error", "Could not delete folder: " + itemName);
|
||||
}
|
||||
} else {
|
||||
QFile file(itemPath);
|
||||
if (file.remove()) {
|
||||
refreshTree();
|
||||
} else {
|
||||
QMessageBox::warning(this, "Error", "Could not delete file: " + itemName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeWidget::renameItem()
|
||||
{
|
||||
if (!m_contextItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString oldPath = getItemPath(m_contextItem);
|
||||
QString oldName = m_contextItem->text(0);
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(this, "Rename", "New name:", QLineEdit::Normal, oldName, &ok);
|
||||
|
||||
if (ok && !newName.isEmpty() && newName != oldName) {
|
||||
QString newPath = QFileInfo(oldPath).absolutePath() + "/" + newName;
|
||||
|
||||
if (QFile::rename(oldPath, newPath)) {
|
||||
refreshTree();
|
||||
} else {
|
||||
QMessageBox::warning(this, "Error", "Could not rename: " + oldName);
|
||||
}
|
||||
}
|
||||
}
|
338
src/MainWindow.cpp
Normal file
338
src/MainWindow.cpp
Normal file
@@ -0,0 +1,338 @@
|
||||
#include "MainWindow.h"
|
||||
#include "TextEditor.h"
|
||||
#include "FileTreeWidget.h"
|
||||
#include "TerminalWidget.h"
|
||||
#include "ProjectManager.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QAction>
|
||||
#include <QtWidgets/QMenuBar>
|
||||
#include <QtWidgets/QToolBar>
|
||||
#include <QtWidgets/QStatusBar>
|
||||
#include <QtWidgets/QSplitter>
|
||||
#include <QtWidgets/QTabWidget>
|
||||
#include <QtWidgets/QVBoxLayout>
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtCore/QStandardPaths>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, m_centralSplitter(nullptr)
|
||||
, m_leftSplitter(nullptr)
|
||||
, m_editorTabs(nullptr)
|
||||
, m_fileTree(nullptr)
|
||||
, m_terminal(nullptr)
|
||||
, m_projectManager(nullptr)
|
||||
, m_settings(nullptr)
|
||||
, m_darkTheme(false)
|
||||
{
|
||||
setWindowTitle("DTC C/C++ IDE");
|
||||
resize(1200, 800);
|
||||
|
||||
// Initialize settings
|
||||
m_settings = new QSettings(this);
|
||||
|
||||
// Initialize project manager
|
||||
m_projectManager = new ProjectManager(this);
|
||||
|
||||
setupUI();
|
||||
setupMenus();
|
||||
setupToolbar();
|
||||
setupStatusBar();
|
||||
connectSignals();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
void MainWindow::setupUI()
|
||||
{
|
||||
// Create central splitter
|
||||
m_centralSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
setCentralWidget(m_centralSplitter);
|
||||
// Create left splitter for file tree and terminal
|
||||
m_leftSplitter = new QSplitter(Qt::Vertical, this);
|
||||
// Create file tree
|
||||
m_fileTree = new FileTreeWidget(this);
|
||||
m_leftSplitter->addWidget(m_fileTree);
|
||||
// Create terminal
|
||||
m_terminal = new TerminalWidget(this);
|
||||
m_leftSplitter->addWidget(m_terminal);
|
||||
// Set splitter proportions
|
||||
m_leftSplitter->setStretchFactor(0, 3);
|
||||
m_leftSplitter->setStretchFactor(1, 1);
|
||||
// Create editor tabs
|
||||
m_editorTabs = new QTabWidget(this);
|
||||
m_editorTabs->setTabsClosable(true);
|
||||
m_editorTabs->setMovable(true);
|
||||
// Add widgets to main splitter
|
||||
m_centralSplitter->addWidget(m_leftSplitter);
|
||||
m_centralSplitter->addWidget(m_editorTabs);
|
||||
// Set splitter proportions
|
||||
m_centralSplitter->setStretchFactor(0, 1);
|
||||
m_centralSplitter->setStretchFactor(1, 3);
|
||||
// VS Code style: set background and border for left sidebar
|
||||
m_leftSplitter->setStyleSheet("QSplitter { background: #23272e; border-right: 1px solid #222; }");
|
||||
m_editorTabs->setStyleSheet("QTabWidget::pane { background: #1e1e1e; border: none; } QTabBar::tab { background: #23272e; color: #d4d4d4; padding: 8px 16px; border-radius: 4px 4px 0 0; } QTabBar::tab:selected { background: #1e1e1e; color: #fff; }");
|
||||
setStyleSheet("QMainWindow { background: #1e1e1e; } QMenuBar { background: #23272e; color: #d4d4d4; } QStatusBar { background: #23272e; color: #d4d4d4; border-top: 1px solid #222; }");
|
||||
}
|
||||
|
||||
void MainWindow::setupMenus()
|
||||
{
|
||||
// File menu
|
||||
QMenu *fileMenu = menuBar()->addMenu("&File");
|
||||
|
||||
QAction *newAction = fileMenu->addAction("&New", this, &MainWindow::newFile);
|
||||
newAction->setShortcut(QKeySequence::New);
|
||||
|
||||
QAction *openAction = fileMenu->addAction("&Open", this, &MainWindow::openFile);
|
||||
openAction->setShortcut(QKeySequence::Open);
|
||||
|
||||
fileMenu->addSeparator();
|
||||
|
||||
QAction *saveAction = fileMenu->addAction("&Save", this, &MainWindow::saveFile);
|
||||
saveAction->setShortcut(QKeySequence::Save);
|
||||
|
||||
QAction *saveAsAction = fileMenu->addAction("Save &As...", this, &MainWindow::saveAsFile);
|
||||
saveAsAction->setShortcut(QKeySequence::SaveAs);
|
||||
|
||||
fileMenu->addSeparator();
|
||||
|
||||
QAction *openProjectAction = fileMenu->addAction("Open &Project...", this, &MainWindow::openProject);
|
||||
openProjectAction->setShortcut(QKeySequence("Ctrl+Shift+O"));
|
||||
|
||||
fileMenu->addSeparator();
|
||||
|
||||
QAction *exitAction = fileMenu->addAction("E&xit", this, &QWidget::close);
|
||||
exitAction->setShortcut(QKeySequence::Quit);
|
||||
|
||||
// Build menu
|
||||
QMenu *buildMenu = menuBar()->addMenu("&Build");
|
||||
|
||||
QAction *buildAction = buildMenu->addAction("&Build Project", this, &MainWindow::buildProject);
|
||||
buildAction->setShortcut(QKeySequence("Ctrl+B"));
|
||||
|
||||
QAction *runAction = buildMenu->addAction("&Run", this, &MainWindow::runProject);
|
||||
runAction->setShortcut(QKeySequence("Ctrl+R"));
|
||||
|
||||
QAction *debugAction = buildMenu->addAction("&Debug", this, &MainWindow::debugProject);
|
||||
debugAction->setShortcut(QKeySequence("Ctrl+D"));
|
||||
|
||||
// View menu
|
||||
QMenu *viewMenu = menuBar()->addMenu("&View");
|
||||
|
||||
QAction *themeAction = viewMenu->addAction("Toggle &Theme", this, &MainWindow::toggleTheme);
|
||||
themeAction->setShortcut(QKeySequence("Ctrl+T"));
|
||||
|
||||
// Help menu
|
||||
QMenu *helpMenu = menuBar()->addMenu("&Help");
|
||||
|
||||
QAction *aboutAction = helpMenu->addAction("&About", this, &MainWindow::showAbout);
|
||||
}
|
||||
|
||||
void MainWindow::setupToolbar()
|
||||
{
|
||||
QToolBar *toolbar = addToolBar("Main");
|
||||
|
||||
toolbar->addAction("New", this, &MainWindow::newFile);
|
||||
toolbar->addAction("Open", this, &MainWindow::openFile);
|
||||
toolbar->addAction("Save", this, &MainWindow::saveFile);
|
||||
toolbar->addSeparator();
|
||||
toolbar->addAction("Build", this, &MainWindow::buildProject);
|
||||
toolbar->addAction("Run", this, &MainWindow::runProject);
|
||||
toolbar->addAction("Debug", this, &MainWindow::debugProject);
|
||||
}
|
||||
|
||||
void MainWindow::setupStatusBar()
|
||||
{
|
||||
statusBar()->showMessage("Ready");
|
||||
}
|
||||
|
||||
void MainWindow::connectSignals()
|
||||
{
|
||||
// File tree signals
|
||||
connect(m_fileTree, &FileTreeWidget::fileDoubleClicked,
|
||||
this, [this](const QString &filePath) {
|
||||
// Open file in editor
|
||||
TextEditor *editor = new TextEditor(this);
|
||||
if (editor->loadFile(filePath)) {
|
||||
int index = m_editorTabs->addTab(editor, QFileInfo(filePath).fileName());
|
||||
m_editorTabs->setCurrentIndex(index);
|
||||
}
|
||||
});
|
||||
|
||||
// Editor tabs signals
|
||||
connect(m_editorTabs, &QTabWidget::tabCloseRequested,
|
||||
this, [this](int index) {
|
||||
QWidget *widget = m_editorTabs->widget(index);
|
||||
m_editorTabs->removeTab(index);
|
||||
widget->deleteLater();
|
||||
});
|
||||
|
||||
// Project manager signals
|
||||
connect(m_projectManager, &ProjectManager::projectOpened,
|
||||
this, [this](const QString &projectPath) {
|
||||
m_fileTree->setRootPath(projectPath);
|
||||
m_terminal->setWorkingDirectory(projectPath);
|
||||
statusBar()->showMessage(QString("Project opened: %1").arg(projectPath));
|
||||
});
|
||||
|
||||
connect(m_projectManager, &ProjectManager::buildOutput,
|
||||
this, [this](const QString &output) {
|
||||
// Show build output in terminal
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::loadSettings()
|
||||
{
|
||||
// Load window geometry
|
||||
restoreGeometry(m_settings->value("geometry").toByteArray());
|
||||
restoreState(m_settings->value("windowState").toByteArray());
|
||||
|
||||
// Load theme
|
||||
m_darkTheme = m_settings->value("darkTheme", false).toBool();
|
||||
if (m_darkTheme) {
|
||||
toggleTheme();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveSettings()
|
||||
{
|
||||
// Save window geometry
|
||||
m_settings->setValue("geometry", saveGeometry());
|
||||
m_settings->setValue("windowState", saveState());
|
||||
|
||||
// Save theme
|
||||
m_settings->setValue("darkTheme", m_darkTheme);
|
||||
}
|
||||
|
||||
void MainWindow::newFile()
|
||||
{
|
||||
TextEditor *editor = new TextEditor(this);
|
||||
int index = m_editorTabs->addTab(editor, "Untitled");
|
||||
m_editorTabs->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
void MainWindow::openFile()
|
||||
{
|
||||
QString filePath = QFileDialog::getOpenFileName(this,
|
||||
"Open File", QString(),
|
||||
"C/C++ Files (*.c *.cpp *.cxx *.cc *.h *.hpp *.hxx);;All Files (*.*)");
|
||||
|
||||
if (!filePath.isEmpty()) {
|
||||
TextEditor *editor = new TextEditor(this);
|
||||
if (editor->loadFile(filePath)) {
|
||||
int index = m_editorTabs->addTab(editor, QFileInfo(filePath).fileName());
|
||||
m_editorTabs->setCurrentIndex(index);
|
||||
} else {
|
||||
delete editor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveFile()
|
||||
{
|
||||
TextEditor *editor = qobject_cast<TextEditor*>(m_editorTabs->currentWidget());
|
||||
if (editor) {
|
||||
if (editor->filePath().isEmpty()) {
|
||||
saveAsFile();
|
||||
} else {
|
||||
editor->saveFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveAsFile()
|
||||
{
|
||||
TextEditor *editor = qobject_cast<TextEditor*>(m_editorTabs->currentWidget());
|
||||
if (editor) {
|
||||
QString filePath = QFileDialog::getSaveFileName(this,
|
||||
"Save File As", QString(),
|
||||
"C/C++ Files (*.c *.cpp *.cxx *.cc *.h *.hpp *.hxx);;All Files (*.*)");
|
||||
|
||||
if (!filePath.isEmpty()) {
|
||||
if (editor->saveAsFile(filePath)) {
|
||||
int index = m_editorTabs->currentIndex();
|
||||
m_editorTabs->setTabText(index, QFileInfo(filePath).fileName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::openProject()
|
||||
{
|
||||
QString projectPath = QFileDialog::getExistingDirectory(this,
|
||||
"Open Project Directory", QString());
|
||||
|
||||
if (!projectPath.isEmpty()) {
|
||||
m_projectManager->openProject(projectPath);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::buildProject()
|
||||
{
|
||||
if (m_projectManager->hasProject()) {
|
||||
m_projectManager->buildProject();
|
||||
statusBar()->showMessage("Building project...");
|
||||
} else {
|
||||
QMessageBox::warning(this, "No Project", "Please open a project first.");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::runProject()
|
||||
{
|
||||
if (m_projectManager->hasProject()) {
|
||||
m_projectManager->runProject();
|
||||
statusBar()->showMessage("Running project...");
|
||||
} else {
|
||||
QMessageBox::warning(this, "No Project", "Please open a project first.");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::debugProject()
|
||||
{
|
||||
// TODO: Implement debug functionality
|
||||
QMessageBox::information(this, "Debug", "Debug functionality will be implemented in Phase 2.");
|
||||
}
|
||||
|
||||
void MainWindow::showAbout()
|
||||
{
|
||||
QMessageBox::about(this, "About DTC C/C++ IDE",
|
||||
"DTC C/C++ IDE v0.1.0\n\n"
|
||||
"A lightweight native IDE for C/C++ development\n"
|
||||
"optimized for modern macOS systems.\n\n"
|
||||
"Built with Qt 6 and C++20");
|
||||
}
|
||||
|
||||
void MainWindow::toggleTheme()
|
||||
{
|
||||
m_darkTheme = !m_darkTheme;
|
||||
|
||||
if (m_darkTheme) {
|
||||
// Apply dark theme
|
||||
QApplication::setStyle("Fusion");
|
||||
QPalette darkPalette;
|
||||
darkPalette.setColor(QPalette::Window, QColor(53, 53, 53));
|
||||
darkPalette.setColor(QPalette::WindowText, Qt::white);
|
||||
darkPalette.setColor(QPalette::Base, QColor(25, 25, 25));
|
||||
darkPalette.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
|
||||
darkPalette.setColor(QPalette::ToolTipBase, Qt::white);
|
||||
darkPalette.setColor(QPalette::ToolTipText, Qt::white);
|
||||
darkPalette.setColor(QPalette::Text, Qt::white);
|
||||
darkPalette.setColor(QPalette::Button, QColor(53, 53, 53));
|
||||
darkPalette.setColor(QPalette::ButtonText, Qt::white);
|
||||
darkPalette.setColor(QPalette::BrightText, Qt::red);
|
||||
darkPalette.setColor(QPalette::Link, QColor(42, 130, 218));
|
||||
darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
|
||||
darkPalette.setColor(QPalette::HighlightedText, Qt::black);
|
||||
QApplication::setPalette(darkPalette);
|
||||
} else {
|
||||
// Apply light theme
|
||||
QApplication::setPalette(QApplication::style()->standardPalette());
|
||||
}
|
||||
}
|
356
src/ProjectManager.cpp
Normal file
356
src/ProjectManager.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
#include "ProjectManager.h"
|
||||
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QTextStream>
|
||||
#include <QtCore/QStandardPaths>
|
||||
|
||||
ProjectManager::ProjectManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_projectType(Unknown)
|
||||
, m_buildProcess(nullptr)
|
||||
, m_isBuilding(false)
|
||||
{
|
||||
m_buildProcess = new QProcess(this);
|
||||
|
||||
connect(m_buildProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, &ProjectManager::onBuildProcessFinished);
|
||||
connect(m_buildProcess, &QProcess::errorOccurred,
|
||||
this, &ProjectManager::onBuildProcessError);
|
||||
connect(m_buildProcess, &QProcess::readyReadStandardOutput,
|
||||
this, &ProjectManager::onBuildOutput);
|
||||
connect(m_buildProcess, &QProcess::readyReadStandardError,
|
||||
this, &ProjectManager::onBuildOutput);
|
||||
}
|
||||
|
||||
ProjectManager::~ProjectManager()
|
||||
{
|
||||
if (m_buildProcess && m_buildProcess->state() == QProcess::Running) {
|
||||
m_buildProcess->kill();
|
||||
m_buildProcess->waitForFinished(3000);
|
||||
}
|
||||
}
|
||||
|
||||
bool ProjectManager::openProject(const QString &projectPath)
|
||||
{
|
||||
if (!QDir(projectPath).exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
closeProject();
|
||||
|
||||
m_projectPath = projectPath;
|
||||
m_projectName = QFileInfo(projectPath).baseName();
|
||||
|
||||
detectProjectType();
|
||||
setupBuildDirectory();
|
||||
|
||||
emit projectOpened(projectPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProjectManager::closeProject()
|
||||
{
|
||||
if (m_buildProcess && m_buildProcess->state() == QProcess::Running) {
|
||||
m_buildProcess->kill();
|
||||
m_buildProcess->waitForFinished(3000);
|
||||
}
|
||||
|
||||
m_projectPath.clear();
|
||||
m_projectName.clear();
|
||||
m_buildDirectory.clear();
|
||||
m_projectType = Unknown;
|
||||
|
||||
emit projectClosed();
|
||||
}
|
||||
|
||||
QString ProjectManager::projectPath() const
|
||||
{
|
||||
return m_projectPath;
|
||||
}
|
||||
|
||||
QString ProjectManager::projectName() const
|
||||
{
|
||||
return m_projectName;
|
||||
}
|
||||
|
||||
bool ProjectManager::hasProject() const
|
||||
{
|
||||
return !m_projectPath.isEmpty();
|
||||
}
|
||||
|
||||
bool ProjectManager::isCMakeProject() const
|
||||
{
|
||||
return m_projectType == CMake;
|
||||
}
|
||||
|
||||
bool ProjectManager::isQMakeProject() const
|
||||
{
|
||||
return m_projectType == QMake;
|
||||
}
|
||||
|
||||
void ProjectManager::buildProject()
|
||||
{
|
||||
if (m_projectPath.isEmpty() || m_isBuilding) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_isBuilding = true;
|
||||
emit buildStarted();
|
||||
|
||||
QStringList arguments;
|
||||
QString program;
|
||||
|
||||
if (m_projectType == CMake) {
|
||||
// Build CMake project
|
||||
program = "cmake";
|
||||
arguments << "--build" << m_buildDirectory;
|
||||
} else if (m_projectType == QMake) {
|
||||
// Build QMake project
|
||||
program = "make";
|
||||
m_buildProcess->setWorkingDirectory(m_buildDirectory);
|
||||
} else {
|
||||
// Try to find and compile C/C++ files directly
|
||||
program = "clang++";
|
||||
QDir sourceDir(m_projectPath);
|
||||
QStringList filters;
|
||||
filters << "*.cpp" << "*.cxx" << "*.cc" << "*.c";
|
||||
QFileInfoList sourceFiles = sourceDir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
if (!sourceFiles.isEmpty()) {
|
||||
arguments << "-std=c++20" << "-O2";
|
||||
for (const QFileInfo &file : sourceFiles) {
|
||||
arguments << file.absoluteFilePath();
|
||||
}
|
||||
arguments << "-o" << QDir(m_buildDirectory).absoluteFilePath("main");
|
||||
} else {
|
||||
emit buildOutput("No source files found to build");
|
||||
m_isBuilding = false;
|
||||
emit buildFinished(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_buildProcess->setWorkingDirectory(m_buildDirectory);
|
||||
m_buildProcess->start(program, arguments);
|
||||
|
||||
if (!m_buildProcess->waitForStarted()) {
|
||||
emit buildOutput("Failed to start build process");
|
||||
m_isBuilding = false;
|
||||
emit buildFinished(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectManager::cleanProject()
|
||||
{
|
||||
if (m_projectPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_projectType == CMake) {
|
||||
// Clean CMake build directory
|
||||
QDir buildDir(m_buildDirectory);
|
||||
buildDir.removeRecursively();
|
||||
setupBuildDirectory();
|
||||
} else if (m_projectType == QMake) {
|
||||
// Clean QMake project
|
||||
QProcess cleanProcess;
|
||||
cleanProcess.setWorkingDirectory(m_buildDirectory);
|
||||
cleanProcess.start("make", QStringList() << "clean");
|
||||
cleanProcess.waitForFinished();
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectManager::configureProject()
|
||||
{
|
||||
if (m_projectPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_projectType == CMake) {
|
||||
// Configure CMake project
|
||||
QProcess configProcess;
|
||||
configProcess.setWorkingDirectory(m_buildDirectory);
|
||||
|
||||
QStringList arguments;
|
||||
arguments << m_projectPath;
|
||||
arguments << "-DCMAKE_BUILD_TYPE=Debug";
|
||||
|
||||
configProcess.start("cmake", arguments);
|
||||
configProcess.waitForFinished();
|
||||
|
||||
emit buildOutput(QString::fromUtf8(configProcess.readAllStandardOutput()));
|
||||
if (!configProcess.readAllStandardError().isEmpty()) {
|
||||
emit buildOutput(QString::fromUtf8(configProcess.readAllStandardError()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectManager::runProject()
|
||||
{
|
||||
if (m_projectPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString executable;
|
||||
|
||||
if (m_projectType == CMake || m_projectType == QMake) {
|
||||
// Look for executable in build directory
|
||||
QDir buildDir(m_buildDirectory);
|
||||
QStringList nameFilters;
|
||||
nameFilters << m_projectName << "main" << "a.out";
|
||||
|
||||
QFileInfoList executables = buildDir.entryInfoList(nameFilters, QDir::Files | QDir::Executable);
|
||||
if (!executables.isEmpty()) {
|
||||
executable = executables.first().absoluteFilePath();
|
||||
}
|
||||
} else {
|
||||
// Look for compiled executable
|
||||
executable = QDir(m_buildDirectory).absoluteFilePath("main");
|
||||
}
|
||||
|
||||
if (executable.isEmpty() || !QFile::exists(executable)) {
|
||||
emit buildOutput("No executable found. Please build the project first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the executable in a new process
|
||||
QProcess *runProcess = new QProcess(this);
|
||||
runProcess->setWorkingDirectory(m_projectPath);
|
||||
|
||||
connect(runProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, [this, runProcess](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
emit buildOutput(QString("Process finished with exit code %1").arg(exitCode));
|
||||
runProcess->deleteLater();
|
||||
});
|
||||
|
||||
connect(runProcess, &QProcess::readyReadStandardOutput,
|
||||
this, [this, runProcess]() {
|
||||
emit buildOutput(QString::fromUtf8(runProcess->readAllStandardOutput()));
|
||||
});
|
||||
|
||||
connect(runProcess, &QProcess::readyReadStandardError,
|
||||
this, [this, runProcess]() {
|
||||
emit buildOutput(QString::fromUtf8(runProcess->readAllStandardError()));
|
||||
});
|
||||
|
||||
runProcess->start(executable);
|
||||
|
||||
if (!runProcess->waitForStarted()) {
|
||||
emit buildOutput("Failed to start executable: " + executable);
|
||||
runProcess->deleteLater();
|
||||
} else {
|
||||
emit buildOutput("Running: " + executable);
|
||||
}
|
||||
}
|
||||
|
||||
QString ProjectManager::buildDirectory() const
|
||||
{
|
||||
return m_buildDirectory;
|
||||
}
|
||||
|
||||
QString ProjectManager::sourceDirectory() const
|
||||
{
|
||||
return m_projectPath;
|
||||
}
|
||||
|
||||
void ProjectManager::onBuildProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
||||
{
|
||||
m_isBuilding = false;
|
||||
|
||||
bool success = (exitStatus == QProcess::NormalExit && exitCode == 0);
|
||||
|
||||
if (success) {
|
||||
emit buildOutput("Build completed successfully");
|
||||
} else {
|
||||
emit buildOutput(QString("Build failed with exit code %1").arg(exitCode));
|
||||
}
|
||||
|
||||
emit buildFinished(success);
|
||||
}
|
||||
|
||||
void ProjectManager::onBuildProcessError(QProcess::ProcessError error)
|
||||
{
|
||||
m_isBuilding = false;
|
||||
|
||||
QString errorString;
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
errorString = "Build process failed to start";
|
||||
break;
|
||||
case QProcess::Crashed:
|
||||
errorString = "Build process crashed";
|
||||
break;
|
||||
default:
|
||||
errorString = "Build process error";
|
||||
break;
|
||||
}
|
||||
|
||||
emit buildOutput("Error: " + errorString);
|
||||
emit buildFinished(false);
|
||||
}
|
||||
|
||||
void ProjectManager::onBuildOutput()
|
||||
{
|
||||
QByteArray standardOutput = m_buildProcess->readAllStandardOutput();
|
||||
if (!standardOutput.isEmpty()) {
|
||||
emit buildOutput(QString::fromUtf8(standardOutput));
|
||||
}
|
||||
|
||||
QByteArray standardError = m_buildProcess->readAllStandardError();
|
||||
if (!standardError.isEmpty()) {
|
||||
emit buildOutput(QString::fromUtf8(standardError));
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectManager::detectProjectType()
|
||||
{
|
||||
if (!findCMakeListsFile(m_projectPath).isEmpty()) {
|
||||
m_projectType = CMake;
|
||||
} else if (!findQMakeFile(m_projectPath).isEmpty()) {
|
||||
m_projectType = QMake;
|
||||
} else {
|
||||
m_projectType = Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectManager::setupBuildDirectory()
|
||||
{
|
||||
if (m_projectPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_buildDirectory = QDir(m_projectPath).absoluteFilePath("build");
|
||||
|
||||
// Create build directory if it doesn't exist
|
||||
QDir().mkpath(m_buildDirectory);
|
||||
|
||||
// Configure project if needed
|
||||
if (m_projectType == CMake) {
|
||||
QString cmakeCache = QDir(m_buildDirectory).absoluteFilePath("CMakeCache.txt");
|
||||
if (!QFile::exists(cmakeCache)) {
|
||||
configureProject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString ProjectManager::findCMakeListsFile(const QString &directory) const
|
||||
{
|
||||
QDir dir(directory);
|
||||
if (dir.exists("CMakeLists.txt")) {
|
||||
return dir.absoluteFilePath("CMakeLists.txt");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString ProjectManager::findQMakeFile(const QString &directory) const
|
||||
{
|
||||
QDir dir(directory);
|
||||
QStringList nameFilters;
|
||||
nameFilters << "*.pro";
|
||||
|
||||
QFileInfoList proFiles = dir.entryInfoList(nameFilters, QDir::Files);
|
||||
if (!proFiles.isEmpty()) {
|
||||
return proFiles.first().absoluteFilePath();
|
||||
}
|
||||
return QString();
|
||||
}
|
197
src/SyntaxHighlighter.cpp
Normal file
197
src/SyntaxHighlighter.cpp
Normal file
@@ -0,0 +1,197 @@
|
||||
#include "SyntaxHighlighter.h"
|
||||
|
||||
SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent)
|
||||
: QSyntaxHighlighter(parent)
|
||||
, m_language("cpp")
|
||||
{
|
||||
setupCppHighlighting();
|
||||
}
|
||||
|
||||
SyntaxHighlighter::~SyntaxHighlighter()
|
||||
{
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::setLanguage(const QString &language)
|
||||
{
|
||||
if (m_language == language) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_language = language;
|
||||
m_highlightingRules.clear();
|
||||
|
||||
if (language == "cpp") {
|
||||
setupCppHighlighting();
|
||||
} else if (language == "c") {
|
||||
setupCHighlighting();
|
||||
} else {
|
||||
setupCommonRules();
|
||||
}
|
||||
|
||||
rehighlight();
|
||||
}
|
||||
|
||||
QString SyntaxHighlighter::language() const
|
||||
{
|
||||
return m_language;
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
// Apply all highlighting rules
|
||||
foreach (const HighlightingRule &rule, m_highlightingRules) {
|
||||
QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text);
|
||||
while (matchIterator.hasNext()) {
|
||||
QRegularExpressionMatch match = matchIterator.next();
|
||||
setFormat(match.capturedStart(), match.capturedLength(), rule.format);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle multi-line comments
|
||||
setCurrentBlockState(0);
|
||||
|
||||
QRegularExpressionMatch startMatch = m_commentStartExpression.match(text);
|
||||
int startIndex = 0;
|
||||
|
||||
if (previousBlockState() != 1)
|
||||
startIndex = startMatch.capturedStart();
|
||||
|
||||
while (startIndex >= 0) {
|
||||
QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, startIndex);
|
||||
int commentLength = 0;
|
||||
|
||||
if (endMatch.hasMatch()) {
|
||||
commentLength = endMatch.capturedStart() + endMatch.capturedLength() - startIndex;
|
||||
} else {
|
||||
setCurrentBlockState(1);
|
||||
commentLength = text.length() - startIndex;
|
||||
}
|
||||
|
||||
setFormat(startIndex, commentLength, m_multiLineCommentFormat);
|
||||
startIndex = m_commentStartExpression.match(text, startIndex + commentLength).capturedStart();
|
||||
}
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::setupCppHighlighting()
|
||||
{
|
||||
setupCommonRules();
|
||||
|
||||
HighlightingRule rule;
|
||||
|
||||
// C++ specific keywords
|
||||
m_keywordFormat.setForeground(QBrush(QColor(86, 156, 214))); // VS Code blue
|
||||
m_keywordFormat.setFontWeight(QFont::Bold);
|
||||
|
||||
QStringList cppKeywords;
|
||||
cppKeywords << "\\bclass\\b" << "\\bnamespace\\b" << "\\btemplate\\b"
|
||||
<< "\\btypename\\b" << "\\bpublic\\b" << "\\bprivate\\b"
|
||||
<< "\\bprotected\\b" << "\\bvirtual\\b" << "\\boverride\\b"
|
||||
<< "\\bfinal\\b" << "\\bexplicit\\b" << "\\binline\\b"
|
||||
<< "\\boperator\\b" << "\\bfriend\\b" << "\\busing\\b"
|
||||
<< "\\bauto\\b" << "\\bdecltype\\b" << "\\bconstexpr\\b"
|
||||
<< "\\bnoexcept\\b" << "\\bnullptr\\b" << "\\bthread_local\\b"
|
||||
<< "\\balignof\\b" << "\\balignас\\b" << "\\bstatic_assert\\b";
|
||||
|
||||
foreach (const QString &pattern, cppKeywords) {
|
||||
rule.pattern = QRegularExpression(pattern);
|
||||
rule.format = m_keywordFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
}
|
||||
|
||||
// STL containers and types
|
||||
m_classFormat.setForeground(QBrush(QColor(78, 201, 176))); // Teal
|
||||
m_classFormat.setFontWeight(QFont::Bold);
|
||||
|
||||
QStringList stlTypes;
|
||||
stlTypes << "\\bstd\\b" << "\\bvector\\b" << "\\bstring\\b" << "\\bmap\\b"
|
||||
<< "\\bset\\b" << "\\blist\\b" << "\\bqueue\\b" << "\\bstack\\b"
|
||||
<< "\\bunordered_map\\b" << "\\bunordered_set\\b" << "\\barray\\b"
|
||||
<< "\\bpair\\b" << "\\btuple\\b" << "\\bshared_ptr\\b"
|
||||
<< "\\bunique_ptr\\b" << "\\bweak_ptr\\b" << "\\boptional\\b";
|
||||
|
||||
foreach (const QString &pattern, stlTypes) {
|
||||
rule.pattern = QRegularExpression(pattern);
|
||||
rule.format = m_classFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
}
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::setupCHighlighting()
|
||||
{
|
||||
setupCommonRules();
|
||||
|
||||
// C doesn't have classes, namespaces, etc., so fewer keywords
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::setupCommonRules()
|
||||
{
|
||||
HighlightingRule rule;
|
||||
|
||||
// Keywords common to C and C++
|
||||
m_keywordFormat.setForeground(QBrush(QColor(86, 156, 214))); // VS Code blue
|
||||
m_keywordFormat.setFontWeight(QFont::Bold);
|
||||
|
||||
QStringList keywords;
|
||||
keywords << "\\bif\\b" << "\\belse\\b" << "\\bfor\\b" << "\\bwhile\\b"
|
||||
<< "\\bdo\\b" << "\\bswitch\\b" << "\\bcase\\b" << "\\bdefault\\b"
|
||||
<< "\\bbreak\\b" << "\\bcontinue\\b" << "\\breturn\\b" << "\\bgoto\\b"
|
||||
<< "\\bint\\b" << "\\bfloat\\b" << "\\bdouble\\b" << "\\bchar\\b"
|
||||
<< "\\bvoid\\b" << "\\blong\\b" << "\\bshort\\b" << "\\bunsigned\\b"
|
||||
<< "\\bsigned\\b" << "\\bconst\\b" << "\\bvolatile\\b" << "\\bstatic\\b"
|
||||
<< "\\bextern\\b" << "\\bregister\\b" << "\\btypedef\\b" << "\\bstruct\\b"
|
||||
<< "\\bunion\\b" << "\\benum\\b" << "\\bsizeof\\b" << "\\btrue\\b"
|
||||
<< "\\bfalse\\b" << "\\bNULL\\b";
|
||||
|
||||
foreach (const QString &pattern, keywords) {
|
||||
rule.pattern = QRegularExpression(pattern);
|
||||
rule.format = m_keywordFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
}
|
||||
|
||||
// Preprocessor directives
|
||||
m_preprocessorFormat.setForeground(QBrush(QColor(155, 155, 155))); // Gray
|
||||
rule.pattern = QRegularExpression("^\\s*#.*");
|
||||
rule.format = m_preprocessorFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
|
||||
// Numbers
|
||||
m_numberFormat.setForeground(QBrush(QColor(181, 206, 168))); // Light green
|
||||
rule.pattern = QRegularExpression("\\b\\d+(\\.\\d+)?([eE][+-]?\\d+)?[fFlL]?\\b");
|
||||
rule.format = m_numberFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
|
||||
rule.pattern = QRegularExpression("\\b0[xX][0-9A-Fa-f]+[uUlL]*\\b");
|
||||
rule.format = m_numberFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
|
||||
// String literals
|
||||
m_quotationFormat.setForeground(QBrush(QColor(206, 145, 120))); // Orange
|
||||
rule.pattern = QRegularExpression("\".*\"");
|
||||
rule.format = m_quotationFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
|
||||
// Character literals
|
||||
rule.pattern = QRegularExpression("'.*'");
|
||||
rule.format = m_quotationFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
|
||||
// Function calls
|
||||
m_functionFormat.setForeground(QBrush(QColor(220, 220, 170))); // Light yellow
|
||||
rule.pattern = QRegularExpression("\\b[A-Za-z_][A-Za-z0-9_]*(?=\\s*\\()");
|
||||
rule.format = m_functionFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
|
||||
// Single line comments
|
||||
m_singleLineCommentFormat.setForeground(QBrush(QColor(106, 153, 85))); // Green
|
||||
m_singleLineCommentFormat.setFontItalic(true);
|
||||
rule.pattern = QRegularExpression("//[^\n]*");
|
||||
rule.format = m_singleLineCommentFormat;
|
||||
m_highlightingRules.append(rule);
|
||||
|
||||
// Multi-line comments
|
||||
m_multiLineCommentFormat.setForeground(QBrush(QColor(106, 153, 85))); // Green
|
||||
m_multiLineCommentFormat.setFontItalic(true);
|
||||
|
||||
m_commentStartExpression = QRegularExpression("/\\*");
|
||||
m_commentEndExpression = QRegularExpression("\\*/");
|
||||
}
|
268
src/TerminalWidget.cpp
Normal file
268
src/TerminalWidget.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
#include "TerminalWidget.h"
|
||||
|
||||
#include <QtWidgets/QVBoxLayout>
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
#include <QtWidgets/QPushButton>
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <QtCore/QDir>
|
||||
|
||||
TerminalWidget::TerminalWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_output(nullptr)
|
||||
, m_input(nullptr)
|
||||
, m_clearButton(nullptr)
|
||||
, m_process(nullptr)
|
||||
, m_normalColor(Qt::white)
|
||||
, m_errorColor(Qt::red)
|
||||
, m_successColor(Qt::green)
|
||||
, m_promptColor(Qt::cyan)
|
||||
{
|
||||
setupUI();
|
||||
setupProcess();
|
||||
setWorkingDirectory(QDir::homePath());
|
||||
}
|
||||
|
||||
TerminalWidget::~TerminalWidget()
|
||||
{
|
||||
if (m_process && m_process->state() == QProcess::Running) {
|
||||
m_process->kill();
|
||||
m_process->waitForFinished(3000);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalWidget::setupUI()
|
||||
{
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(5, 5, 5, 5);
|
||||
layout->setSpacing(5);
|
||||
|
||||
// Output area
|
||||
m_output = new QTextEdit(this);
|
||||
m_output->setReadOnly(true);
|
||||
m_output->setFont(QFont("Monaco", 11));
|
||||
m_output->setStyleSheet("QTextEdit { background-color: #1e1e1e; color: #ffffff; }");
|
||||
layout->addWidget(m_output);
|
||||
|
||||
// Input area
|
||||
QHBoxLayout *inputLayout = new QHBoxLayout();
|
||||
|
||||
m_input = new QLineEdit(this);
|
||||
m_input->setFont(QFont("Monaco", 11));
|
||||
m_input->setStyleSheet("QLineEdit { background-color: #2d2d2d; color: #ffffff; border: 1px solid #555; }");
|
||||
inputLayout->addWidget(m_input);
|
||||
|
||||
m_clearButton = new QPushButton("Clear", this);
|
||||
m_clearButton->setMaximumWidth(80);
|
||||
inputLayout->addWidget(m_clearButton);
|
||||
|
||||
layout->addLayout(inputLayout);
|
||||
|
||||
// Connect signals
|
||||
connect(m_input, &QLineEdit::returnPressed, this, &TerminalWidget::onCommandEntered);
|
||||
connect(m_clearButton, &QPushButton::clicked, this, &TerminalWidget::clear);
|
||||
|
||||
// Initial prompt
|
||||
appendPrompt();
|
||||
}
|
||||
|
||||
void TerminalWidget::setupProcess()
|
||||
{
|
||||
m_process = new QProcess(this);
|
||||
|
||||
connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, &TerminalWidget::onProcessFinished);
|
||||
connect(m_process, &QProcess::errorOccurred,
|
||||
this, &TerminalWidget::onProcessError);
|
||||
connect(m_process, &QProcess::readyReadStandardOutput,
|
||||
this, &TerminalWidget::onProcessOutput);
|
||||
connect(m_process, &QProcess::readyReadStandardError,
|
||||
this, &TerminalWidget::onProcessOutput);
|
||||
}
|
||||
|
||||
void TerminalWidget::setWorkingDirectory(const QString &directory)
|
||||
{
|
||||
m_workingDirectory = directory;
|
||||
updatePrompt();
|
||||
}
|
||||
|
||||
QString TerminalWidget::workingDirectory() const
|
||||
{
|
||||
return m_workingDirectory;
|
||||
}
|
||||
|
||||
void TerminalWidget::executeCommand(const QString &command)
|
||||
{
|
||||
if (command.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show command in output
|
||||
appendOutput(m_currentPrompt + command, m_normalColor);
|
||||
|
||||
// Handle built-in commands
|
||||
if (command == "clear") {
|
||||
clear();
|
||||
return;
|
||||
} else if (command.startsWith("cd ")) {
|
||||
QString newDir = command.mid(3).trimmed();
|
||||
if (newDir.isEmpty()) {
|
||||
newDir = QDir::homePath();
|
||||
} else if (!QDir::isAbsolutePath(newDir)) {
|
||||
newDir = QDir(m_workingDirectory).absoluteFilePath(newDir);
|
||||
}
|
||||
|
||||
if (QDir(newDir).exists()) {
|
||||
setWorkingDirectory(newDir);
|
||||
appendOutput("", m_successColor);
|
||||
} else {
|
||||
appendOutput("cd: no such file or directory: " + newDir, m_errorColor);
|
||||
}
|
||||
appendPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute external command
|
||||
if (m_process->state() == QProcess::Running) {
|
||||
appendOutput("Process already running. Please wait...", m_errorColor);
|
||||
appendPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
m_process->setWorkingDirectory(m_workingDirectory);
|
||||
|
||||
// Use zsh shell for macOS
|
||||
QStringList arguments;
|
||||
arguments << "-c" << command;
|
||||
|
||||
m_process->start("/bin/zsh", arguments);
|
||||
|
||||
if (!m_process->waitForStarted()) {
|
||||
appendOutput("Failed to start command: " + command, m_errorColor);
|
||||
appendPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalWidget::clear()
|
||||
{
|
||||
m_output->clear();
|
||||
appendPrompt();
|
||||
}
|
||||
|
||||
void TerminalWidget::showBuildOutput()
|
||||
{
|
||||
appendOutput("\n=== Build Output ===", m_promptColor);
|
||||
}
|
||||
|
||||
void TerminalWidget::showRunOutput()
|
||||
{
|
||||
appendOutput("\n=== Run Output ===", m_promptColor);
|
||||
}
|
||||
|
||||
void TerminalWidget::onCommandEntered()
|
||||
{
|
||||
QString command = m_input->text().trimmed();
|
||||
m_input->clear();
|
||||
|
||||
executeCommand(command);
|
||||
}
|
||||
|
||||
void TerminalWidget::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
||||
{
|
||||
if (exitStatus == QProcess::CrashExit) {
|
||||
appendOutput("Process crashed", m_errorColor);
|
||||
} else {
|
||||
if (exitCode == 0) {
|
||||
// Success - don't show anything for clean exit
|
||||
} else {
|
||||
appendOutput(QString("Process finished with exit code %1").arg(exitCode), m_errorColor);
|
||||
}
|
||||
}
|
||||
|
||||
appendPrompt();
|
||||
emit commandFinished(exitCode);
|
||||
}
|
||||
|
||||
void TerminalWidget::onProcessError(QProcess::ProcessError error)
|
||||
{
|
||||
QString errorString;
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
errorString = "Failed to start process";
|
||||
break;
|
||||
case QProcess::Crashed:
|
||||
errorString = "Process crashed";
|
||||
break;
|
||||
case QProcess::Timedout:
|
||||
errorString = "Process timed out";
|
||||
break;
|
||||
case QProcess::WriteError:
|
||||
errorString = "Write error";
|
||||
break;
|
||||
case QProcess::ReadError:
|
||||
errorString = "Read error";
|
||||
break;
|
||||
case QProcess::UnknownError:
|
||||
errorString = "Unknown error";
|
||||
break;
|
||||
}
|
||||
|
||||
appendOutput("Error: " + errorString, m_errorColor);
|
||||
appendPrompt();
|
||||
}
|
||||
|
||||
void TerminalWidget::onProcessOutput()
|
||||
{
|
||||
// Read standard output
|
||||
QByteArray standardOutput = m_process->readAllStandardOutput();
|
||||
if (!standardOutput.isEmpty()) {
|
||||
appendOutput(QString::fromUtf8(standardOutput), m_normalColor);
|
||||
}
|
||||
|
||||
// Read standard error
|
||||
QByteArray standardError = m_process->readAllStandardError();
|
||||
if (!standardError.isEmpty()) {
|
||||
appendOutput(QString::fromUtf8(standardError), m_errorColor);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalWidget::updatePrompt()
|
||||
{
|
||||
QString dirName = QDir(m_workingDirectory).dirName();
|
||||
if (dirName.isEmpty()) {
|
||||
dirName = "/";
|
||||
}
|
||||
m_currentPrompt = QString("%1 $ ").arg(dirName);
|
||||
}
|
||||
|
||||
void TerminalWidget::appendOutput(const QString &text, const QColor &color)
|
||||
{
|
||||
QTextCursor cursor = m_output->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
|
||||
QTextCharFormat format;
|
||||
format.setForeground(color);
|
||||
cursor.setCharFormat(format);
|
||||
|
||||
cursor.insertText(text + "\n");
|
||||
|
||||
// Auto-scroll to bottom
|
||||
m_output->setTextCursor(cursor);
|
||||
m_output->ensureCursorVisible();
|
||||
}
|
||||
|
||||
void TerminalWidget::appendPrompt()
|
||||
{
|
||||
updatePrompt();
|
||||
|
||||
QTextCursor cursor = m_output->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
|
||||
QTextCharFormat format;
|
||||
format.setForeground(m_promptColor);
|
||||
cursor.setCharFormat(format);
|
||||
|
||||
cursor.insertText(m_currentPrompt);
|
||||
|
||||
m_output->setTextCursor(cursor);
|
||||
m_output->ensureCursorVisible();
|
||||
}
|
282
src/TextEditor.cpp
Normal file
282
src/TextEditor.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
#include "TextEditor.h"
|
||||
#include "SyntaxHighlighter.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtGui/QTextBlock>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QTextStream>
|
||||
|
||||
TextEditor::TextEditor(QWidget *parent)
|
||||
: QPlainTextEdit(parent)
|
||||
, m_isModified(false)
|
||||
, m_syntaxHighlighter(nullptr)
|
||||
, m_lineNumberArea(nullptr)
|
||||
, m_autoSaveTimer(nullptr)
|
||||
{
|
||||
setupEditor();
|
||||
}
|
||||
|
||||
TextEditor::~TextEditor()
|
||||
{
|
||||
}
|
||||
|
||||
void TextEditor::setupEditor()
|
||||
{
|
||||
// Set font
|
||||
QFont font("Monaco", 12);
|
||||
font.setFixedPitch(true);
|
||||
setFont(font);
|
||||
|
||||
// Set tab stop width
|
||||
QFontMetrics metrics(font);
|
||||
setTabStopDistance(4 * metrics.horizontalAdvance(' '));
|
||||
|
||||
// Create line number area
|
||||
m_lineNumberArea = new LineNumberArea(this);
|
||||
|
||||
// Setup syntax highlighter
|
||||
m_syntaxHighlighter = new SyntaxHighlighter(document());
|
||||
|
||||
// Setup auto-save timer
|
||||
m_autoSaveTimer = new QTimer(this);
|
||||
m_autoSaveTimer->setSingleShot(true);
|
||||
m_autoSaveTimer->setInterval(5000); // 5 seconds
|
||||
|
||||
// Connect signals
|
||||
connect(document(), &QTextDocument::blockCountChanged,
|
||||
this, &TextEditor::updateLineNumberAreaWidth);
|
||||
connect(verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, [this](int) { m_lineNumberArea->update(); });
|
||||
connect(this, &QPlainTextEdit::updateRequest,
|
||||
this, &TextEditor::updateLineNumberArea);
|
||||
connect(this, &QPlainTextEdit::cursorPositionChanged,
|
||||
this, &TextEditor::highlightCurrentLine);
|
||||
connect(this, &QPlainTextEdit::cursorPositionChanged,
|
||||
this, &TextEditor::updateCursorPosition);
|
||||
connect(document(), &QTextDocument::modificationChanged,
|
||||
this, &TextEditor::setModified);
|
||||
connect(m_autoSaveTimer, &QTimer::timeout,
|
||||
this, &TextEditor::saveFile);
|
||||
|
||||
// Initial setup
|
||||
updateLineNumberAreaWidth(0);
|
||||
highlightCurrentLine();
|
||||
}
|
||||
|
||||
void TextEditor::setFilePath(const QString &filePath)
|
||||
{
|
||||
m_filePath = filePath;
|
||||
|
||||
// Set syntax highlighting based on file extension
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString extension = fileInfo.suffix().toLower();
|
||||
|
||||
if (extension == "cpp" || extension == "cxx" || extension == "cc" ||
|
||||
extension == "hpp" || extension == "hxx") {
|
||||
m_syntaxHighlighter->setLanguage("cpp");
|
||||
} else if (extension == "c" || extension == "h") {
|
||||
m_syntaxHighlighter->setLanguage("c");
|
||||
}
|
||||
}
|
||||
|
||||
QString TextEditor::filePath() const
|
||||
{
|
||||
return m_filePath;
|
||||
}
|
||||
|
||||
bool TextEditor::isModified() const
|
||||
{
|
||||
return m_isModified;
|
||||
}
|
||||
|
||||
void TextEditor::setModified(bool modified)
|
||||
{
|
||||
if (m_isModified != modified) {
|
||||
m_isModified = modified;
|
||||
emit fileModified(modified);
|
||||
|
||||
if (modified && !m_filePath.isEmpty()) {
|
||||
m_autoSaveTimer->start();
|
||||
} else {
|
||||
m_autoSaveTimer->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TextEditor::loadFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream in(&file);
|
||||
setPlainText(in.readAll());
|
||||
setFilePath(filePath);
|
||||
document()->setModified(false);
|
||||
setModified(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextEditor::saveFile()
|
||||
{
|
||||
if (m_filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return saveAsFile(m_filePath);
|
||||
}
|
||||
|
||||
bool TextEditor::saveAsFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
out << toPlainText();
|
||||
|
||||
setFilePath(filePath);
|
||||
document()->setModified(false);
|
||||
setModified(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextEditor::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
autoIndent();
|
||||
} else {
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditor::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPlainTextEdit::paintEvent(event);
|
||||
}
|
||||
|
||||
void TextEditor::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QPlainTextEdit::resizeEvent(event);
|
||||
|
||||
QRect cr = contentsRect();
|
||||
m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(),
|
||||
lineNumberAreaWidth(), cr.height()));
|
||||
}
|
||||
|
||||
void TextEditor::highlightCurrentLine()
|
||||
{
|
||||
QList<QTextEdit::ExtraSelection> extraSelections;
|
||||
|
||||
if (!isReadOnly()) {
|
||||
QTextEdit::ExtraSelection selection;
|
||||
|
||||
QColor lineColor = QColor(Qt::yellow).lighter(160);
|
||||
selection.format.setBackground(lineColor);
|
||||
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
||||
selection.cursor = textCursor();
|
||||
selection.cursor.clearSelection();
|
||||
extraSelections.append(selection);
|
||||
}
|
||||
|
||||
setExtraSelections(extraSelections);
|
||||
}
|
||||
|
||||
void TextEditor::updateCursorPosition()
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
int line = cursor.blockNumber() + 1;
|
||||
int column = cursor.columnNumber() + 1;
|
||||
emit cursorPositionChanged(line, column);
|
||||
}
|
||||
|
||||
void TextEditor::autoIndent()
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextBlock currentBlock = cursor.block();
|
||||
QTextBlock previousBlock = currentBlock.previous();
|
||||
|
||||
if (previousBlock.isValid()) {
|
||||
QString previousText = previousBlock.text();
|
||||
QString indent;
|
||||
|
||||
// Count leading whitespace
|
||||
for (const QChar &ch : previousText) {
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
indent += ch;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra indent for opening braces
|
||||
if (previousText.trimmed().endsWith('{')) {
|
||||
indent += " "; // 4 spaces
|
||||
}
|
||||
|
||||
cursor.insertText(indent);
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditor::updateLineNumberAreaWidth(int newBlockCount)
|
||||
{
|
||||
Q_UNUSED(newBlockCount)
|
||||
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
|
||||
}
|
||||
|
||||
int TextEditor::lineNumberAreaWidth()
|
||||
{
|
||||
int digits = 1;
|
||||
int max = qMax(1, blockCount());
|
||||
while (max >= 10) {
|
||||
max /= 10;
|
||||
++digits;
|
||||
}
|
||||
|
||||
int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
|
||||
return space;
|
||||
}
|
||||
|
||||
void TextEditor::updateLineNumberArea(const QRect &rect, int dy)
|
||||
{
|
||||
if (dy)
|
||||
m_lineNumberArea->scroll(0, dy);
|
||||
else
|
||||
m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
|
||||
|
||||
if (rect.contains(viewport()->rect()))
|
||||
updateLineNumberAreaWidth(0);
|
||||
}
|
||||
|
||||
// LineNumberArea implementation
|
||||
void LineNumberArea::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(event->rect(), QColor(240, 240, 240));
|
||||
|
||||
QTextBlock block = m_textEditor->firstVisibleBlock();
|
||||
int blockNumber = block.blockNumber();
|
||||
int top = m_textEditor->blockBoundingGeometry(block).translated(m_textEditor->contentOffset()).top();
|
||||
int bottom = top + m_textEditor->blockBoundingRect(block).height();
|
||||
|
||||
while (block.isValid() && top <= event->rect().bottom()) {
|
||||
if (block.isVisible() && bottom >= event->rect().top()) {
|
||||
QString number = QString::number(blockNumber + 1);
|
||||
painter.setPen(Qt::gray);
|
||||
painter.drawText(0, top, width(), fontMetrics().height(),
|
||||
Qt::AlignRight, number);
|
||||
}
|
||||
|
||||
block = block.next();
|
||||
top = bottom;
|
||||
bottom = top + m_textEditor->blockBoundingRect(block).height();
|
||||
++blockNumber;
|
||||
}
|
||||
}
|
21
src/main.cpp
Normal file
21
src/main.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// Set application properties
|
||||
app.setApplicationName("DTC C/C++ IDE");
|
||||
app.setApplicationVersion("0.1.0");
|
||||
app.setOrganizationName("DTC");
|
||||
app.setOrganizationDomain("dtc.com");
|
||||
|
||||
// Create main window
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
42
ui/MainWindow.ui
Normal file
42
ui/MainWindow.ui
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1200</width>
|
||||
<height>800</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>DTC C/C++ IDE</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget"/>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1200</width>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QToolBar" name="toolBar">
|
||||
<property name="windowTitle">
|
||||
<string>toolBar</string>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
Reference in New Issue
Block a user