#include "FileTreeWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include 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); } } }