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